# SSL修复 - 必须在其他网络库导入之前执行
try:
    import ssl_fix
except ImportError:
    pass  # 如果ssl_fix模块不存在，忽略导入错误

import sys
import json
import requests as requests_lib  # 重命名为requests_lib以避免冲突
import time
import os
import pymysql
import threading
import logging, os, traceback
from password_utils import decrypt_password, encrypt_password, is_encrypted
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QComboBox, QPushButton, QListWidget, QListWidgetItem, QLabel,
    QScrollArea, QFrame, QSizePolicy, QStackedWidget, QMenuBar, QAction, QMenu,
    QDialog, QTextEdit, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QFileDialog,
    QProgressBar, QMessageBox, QGroupBox, QGridLayout, QTableWidget, QTableWidgetItem, QDateEdit,
    QRadioButton, QButtonGroup
)
from PyQt5.QtWidgets import QHeaderView, QAbstractItemView
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QTimer, QEasingCurve, QPropertyAnimation, QThread
from PyQt5.QtGui import QPixmap, QFont, QPalette, QColor, QTextCursor, QClipboard, QIcon, QTextCharFormat
import requests  # 确保导入requests模块

# 注册元类型，避免跨线程连接警告
# 部分 PyQt5 版本/发行版可能不提供 qRegisterMetaType，这里做兼容并静默失败
try:
    from PyQt5 import QtCore
    from PyQt5.QtGui import QTextCursor, QTextCharFormat
    qRegisterMetaType = getattr(QtCore, "qRegisterMetaType", None)
    if callable(qRegisterMetaType):
        # 先按名称注册
        try:
            qRegisterMetaType("QTextCursor")
            qRegisterMetaType("QTextCharFormat")
        except Exception:
            pass
        # 再尝试按类型对象注册
        try:
            qRegisterMetaType(QTextCursor)
            qRegisterMetaType(QTextCharFormat)
        except Exception:
            pass
except Exception:
    # 如果目标环境没有 qRegisterMetaType，直接跳过即可，不影响正常运行
    pass

# 打包资源路径适配（兼容 PyInstaller onefile）
def resource_path(relative_path: str) -> str:
    try:
        base_path = sys._MEIPASS  # type: ignore[attr-defined]
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

# DPI 缩放辅助
def scaled_size(w: int, h: int):
    try:
        screen = QApplication.primaryScreen()
        dpi = screen.logicalDotsPerInch() if screen else 96.0
        scale = max(1.0, dpi / 96.0)
        return int(w * scale), int(h * scale)
    except Exception:
        return w, h

# 可选中的标题标签类
class SelectableTitleLabel(QLabel):
    text_selected = pyqtSignal(str)  # 文本选中信号
    text_deselected = pyqtSignal()   # 文本取消选中信号
    
    def __init__(self, text=""):
        super().__init__(text)
        self.is_selected = False
        self.setTextFormat(Qt.TextFormat.RichText)
        self.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
        
    def mousePressEvent(self, ev):
        super().mousePressEvent(ev)
        # 检查是否有文本被选中
        if self.hasSelectedText():
            selected_text = self.selectedText()
            if selected_text and not self.is_selected:
                self.is_selected = True
                self.text_selected.emit(selected_text)
        else:
            if self.is_selected:
                self.is_selected = False
                self.text_deselected.emit()
                
    def mouseReleaseEvent(self, ev):
        super().mouseReleaseEvent(ev)
        # 检查是否有文本被选中
        if self.hasSelectedText():
            selected_text = self.selectedText()
            if selected_text and not self.is_selected:
                self.is_selected = True
                self.text_selected.emit(selected_text)
        else:
            if self.is_selected:
                self.is_selected = False
                self.text_deselected.emit()

# 可点击的图片标签类
class ClickableImageLabel(QLabel):
    clicked = pyqtSignal()
    
    def __init__(self):
        super().__init__()
        
    def mousePressEvent(self, ev):
        from PyQt5.QtCore import Qt
        from PyQt5.QtGui import QMouseEvent
        if ev is not None and isinstance(ev, QMouseEvent) and ev.button() == Qt.MouseButton.LeftButton:
            self.clicked.emit()
        super().mousePressEvent(ev)

# 全局图片缓存
_image_cache = {}
_image_cache_max_size = 500
# 全局加载状态记录
_loading_urls = set()  # 最多缓存100张图片


# 导入商品详情页面模块
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

# 运行期日志到文件（便于定位崩溃）
try:
    os.makedirs("logs", exist_ok=True)
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
        handlers=[
            logging.FileHandler("logs/runtime.log", encoding="utf-8"),
        ],
    )
    def _excepthook(tp, value, tb):
        logging.error("Uncaught exception:", exc_info=(tp, value, tb))
        print("Uncaught exception:", value)
    sys.excepthook = _excepthook
except Exception:
    pass

# 导入新的模块
from data_crawler import DataCrawlerAPI, KuaishouAPI
from video_downloader import VideoDownloader
from retry_task_manager import RiskControlRetryManager, RetryTaskControlDialog
from data_cleanup import DataCleanupTask
from config import config
from material import MaterialCrawler, MaterialVideoDownloader
from pollinations import PollinationsGenerator, SiliconFlowGenerator

# 商品信息展示卡片
class GoodsCard(QWidget):
    def __init__(self, goods_data, parent=None):
        super().__init__(parent)
        self.goods_data = goods_data
        self.setup_ui()
        # 右键菜单功能已移除
        
    def setup_ui(self):
        # 设置卡片样式 - 紧凑型风格，减少尺寸以显示更多商品
        self.setStyleSheet("""
            QWidget {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #ffffff, stop:1 #f8faff);
                border: none;
                border-radius: 16px;
                padding: 12px;
            }
            QWidget:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #f0f4ff, stop:1 #e8f2ff);
            }
        """)
        
        layout = QHBoxLayout(self)
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(12)
        
        # 商品图片容器 - 增大尺寸，去掉框线
        image_container = QWidget()
        image_container.setStyleSheet("""
            QWidget {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
                    stop:0 #f5f7fb, stop:1 #e8eef7);
                border-radius: 18px;
                border: none;
            }
        """)
        image_layout = QVBoxLayout(image_container)
        image_layout.setContentsMargins(0, 0, 0, 0)
        
        # 商品图片 - 减小尺寸到180x180以显示更多商品
        self.image_label = ClickableImageLabel()
        self.image_label.setFixedSize(180, 180)  # 减小图片尺寸到180x180
        self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.image_label.setStyleSheet("""
            QLabel {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
                    stop:0 #ffffff, stop:1 #f8faff);
                border-radius: 15px;
                border: none;
                font-size: 14px;
                color: #8891a8;
                font-weight: 500;
            }
        """)
        image_layout.addWidget(self.image_label)
        layout.addWidget(image_container)
        
        # 商品信息
        info_container = QWidget()
        info_layout = QVBoxLayout(info_container)
        info_layout.setSpacing(8)  # 减少间距
        
        # 商品信息
        info_container = QWidget()
        info_layout = QVBoxLayout(info_container)
        info_layout.setSpacing(6)  # 进一步减少间距
        
        # 第一行：商品标题 + 商品ID（右上角）
        title_row = QWidget()
        title_layout = QHBoxLayout(title_row)
        title_layout.setContentsMargins(0, 0, 0, 0)
        title_layout.setSpacing(15)  # 增加间距使布局更清晰
        
        # 商品标题 - 使用可选中的标题标签
        self.title_label = SelectableTitleLabel(self.goods_data.get('itemTitle', ''))
        self.title_label.setWordWrap(True)
        self.title_label.setFont(QFont("Arial", 15, QFont.Bold))  # 再次增大字体到15
        self.title_label.setMaximumHeight(60)  # 限制最大高度，减少占比
        self.title_label.setStyleSheet("""
            QLabel {
                color: #2c3e50;
                line-height: 1.4;
                font-weight: 600;
                letter-spacing: 0.5px;
            }
        """)
        # 连接文本选中信号
        self.title_label.text_selected.connect(self.on_title_selected)
        self.title_label.text_deselected.connect(self.on_title_deselected)
        title_layout.addWidget(self.title_label, 67)  # 标题卥67%，与 ID 的 2:1 占比
        
        # 商品ID区域（右上角）- 将复制按钮集成到ID标签内
        id_widget = QWidget()
        id_widget.setStyleSheet("""
            QWidget {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                    stop:0 #f8faff, stop:1 #e8f0ff);
                border-radius: 10px;
                border: none;
            }
        """)
        id_layout = QHBoxLayout(id_widget)
        id_layout.setContentsMargins(8, 4, 8, 4)  # 内边距
        id_layout.setSpacing(8)  # ID文字和按钮之间的间距
        
        # 商品ID显示
        rel_item_id = self.goods_data.get('relItemId', '')
        # 显示完整的商品ID
        rel_item_id_str = str(rel_item_id)
        self.id_label = QLabel(f"ID: {rel_item_id_str}")
        self.id_label.setStyleSheet("""
            QLabel {
                color: #5a6c7d;
                font-size: 11px;
                font-weight: 500;
                background: transparent;
                border: none;
            }
        """)
        id_layout.addWidget(self.id_label)
        
        # 复制ID按钮 - 放在ID里面
        self.copy_id_btn = QPushButton("复制")
        self.copy_id_btn.setFixedSize(40, 20)  # 减小按钮尺寸以适应ID内部
        self.copy_id_btn.setStyleSheet("""
            QPushButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #667eea, stop:1 #5a67d8);
                color: white;
                border: none;
                border-radius: 8px;
                font-size: 10px;
                font-weight: 600;
                padding: 2px 6px;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #5a67d8, stop:1 #4c51bf);
            }
            QPushButton:pressed {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #4c51bf, stop:1 #434190);
            }
        """)
        self.copy_id_btn.clicked.connect(lambda: self.copy_product_id(rel_item_id))
        id_layout.addWidget(self.copy_id_btn)
        
        id_widget.setMinimumWidth(140)  # 调整最小宽度以适应新布局
        title_layout.addWidget(id_widget, 33)  # ID区域卥33%，实现与标题 2:1 的占比
        
        info_layout.addWidget(title_row)
        
        # 价格和佣金信息区域
        price_widget = QWidget()
        price_layout = QHBoxLayout(price_widget)
        price_layout.setContentsMargins(0, 0, 0, 0)
        price_layout.setSpacing(10)
        
        # 价格信息
        self.price_label = QLabel(f"价格: ¥{self.goods_data.get('zkFinalPrice', self.goods_data.get('reservePrice', ''))}")
        self.price_label.setStyleSheet("""
            QLabel {
                color: #e53e3e;
                font-weight: 700;
                font-size: 16px;
            }
        """)
        price_layout.addWidget(self.price_label)
        
        # 佣金信息
        commission_rate = self.goods_data.get('commissionRate', '')
        self.commission_label = QLabel(f"佣金: {commission_rate}%")
        self.commission_label.setStyleSheet("""
            QLabel {
                color: #2b6cb0;
                font-weight: 600;
                font-size: 12px;
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                    stop:0 #ebf8ff, stop:1 #bee3f8);
                padding: 4px 10px;
                border-radius: 12px;
                border: none;
            }
        """)
        price_layout.addWidget(self.commission_label)
        
        # 赚取金额信息
        profit_amount = self.goods_data.get('profitAmount', '')
        if profit_amount:
            self.profit_label = QLabel(f"赚取: ¥{profit_amount}")
            self.profit_label.setStyleSheet("""
                QLabel {
                    color: #22543d;
                    font-weight: 600;
                    font-size: 12px;
                    background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                        stop:0 #f0fff4, stop:1 #c6f6d5);
                    padding: 4px 10px;
                    border-radius: 12px;
                    border: none;
                }
            """)
            price_layout.addWidget(self.profit_label)
        
        price_layout.addStretch()
        info_layout.addWidget(price_widget)
        
        # 最后一行：店铺名称和销量
        bottom_row = QWidget()
        bottom_layout = QHBoxLayout(bottom_row)
        bottom_layout.setContentsMargins(0, 0, 0, 0)
        bottom_layout.setSpacing(10)
        
        # 店铺名称（删除"店铺:"前缀）
        self.shop_label = QLabel(self.goods_data.get('sellerName', ''))
        self.shop_label.setStyleSheet("""
            QLabel {
                color: #5a6c7d;
                font-size: 12px;
                font-weight: 500;
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                    stop:0 #f7fafc, stop:1 #edf2f7);
                padding: 4px 10px;
                border-radius: 10px;
                border: none;
            }
        """)
        bottom_layout.addWidget(self.shop_label)
        
        # 销量信息
        sales_volume_desc = self.goods_data.get('salesVolumeDesc', {})
        sales_value = sales_volume_desc.get('value', '')
        sales_prefix = sales_volume_desc.get('prefix', '')
        sales_unit = sales_volume_desc.get('unit', '')
        
        # 使用API返回的unit字段，而不是手动添加"万"字
        if sales_value:
            sales_text = f"{sales_prefix}{sales_value}{sales_unit}"
            
            self.sales_label = QLabel(f"销量: {sales_text}")
            self.sales_label.setStyleSheet("""
                QLabel {
                    color: #2b6cb0;
                    font-weight: 600;
                    font-size: 12px;
                    background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                        stop:0 #ebf8ff, stop:1 #bee3f8);
                    padding: 4px 10px;
                    border-radius: 10px;
                    border: none;
                }
            """)
            bottom_layout.addWidget(self.sales_label)
        
        bottom_layout.addStretch()
        info_layout.addWidget(bottom_row)
        
        info_layout.addStretch()
        layout.addWidget(info_container)
        
        # 加载图片
        self.load_image()
        
        # 只为图片添加点击事件，打开商品详情页面
        self.image_label.setCursor(Qt.CursorShape.PointingHandCursor)
        self.image_label.clicked.connect(self.open_product_detail)
        
    def on_title_selected(self, selected_text):
        """处理标题文本选中事件"""
        # 创建巨量百应搜索按钮
        if not hasattr(self, 'search_button'):
            self.search_button = QPushButton("🔍 巨量百应搜索")
            self.search_button.setStyleSheet("""
                QPushButton {
                    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                        stop:0 #ed64a6, stop:1 #d53f8c);
                    color: white;
                    border: none;
                    border-radius: 16px;
                    padding: 8px 16px;
                    font-size: 11px;
                    font-weight: 600;
                    position: absolute;
                    letter-spacing: 0.5px;
                }
                QPushButton:hover {
                    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                        stop:0 #d53f8c, stop:1 #b83280);
                }
                QPushButton:pressed {
                    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                        stop:0 #b83280, stop:1 #97266d);
                }
            """)
            
        # 重置按钮状态为初始状态
        self.search_button.setText("🔍 巨量百应搜索")
        self.search_button.setEnabled(True)
            
        # 更新按钮的点击事件
        try:
            self.search_button.clicked.disconnect()
        except TypeError:
            pass  # 如果没有连接的信号，则忽略错误
        self.search_button.clicked.connect(lambda: self.search_on_juliang(selected_text))
            
        # 根据用户记忆偏好，按钮应该出现在选中文本区域的右上角
        # 将按钮添加到当前卡片中
        self.search_button.setParent(self)
        
        # 获取标题标签的位置和尺寸
        title_geometry = self.title_label.geometry()
        # 将按钮放置在标题区域的右上角
        button_x = title_geometry.x() + title_geometry.width() - 120
        button_y = title_geometry.y() - 5
        self.search_button.move(button_x, button_y)
        self.search_button.resize(120, 30)
        self.search_button.show()
        self.search_button.raise_()
            
    def on_title_deselected(self):
        """处理标题文本取消选中事件"""
        # 隐藏搜索按钮
        if hasattr(self, 'search_button') and self.search_button:
            self.search_button.hide()
            
    def get_main_window(self):
        """获取主窗口"""
        parent = self.parent()
        while parent:
            if isinstance(parent, QMainWindow):
                return parent
            parent = parent.parent()
        return None
        
    def search_on_juliang(self, search_text):
        """在巨量百应中搜索 - 通过FastAPI发送给浏览器插件（后台线程，详细日志）"""
        main = self.get_main_window()
        def log(msg, color="#9CA3AF"):
            try:
                if main and hasattr(main, 'add_log_message'):
                    main.add_log_message(msg, color)
            except Exception:
                pass
        try:
            # 禁用按钮，显示进行中
            if hasattr(self, 'search_button') and self.search_button:
                self.search_button.setText("… 发送中")
                self.search_button.setEnabled(False)

            api_base = "http://127.0.0.1:8767"
            api_url = f"{api_base}/api/search"
            payload = {"search_text": search_text, "source": "pyqt_app"}
            log(f"🔎 [百应搜索] 准备发送：{search_text}")

            # 后台线程执行网络请求，避免卡 UI
            from PyQt5.QtCore import QThread, pyqtSignal
            class SearchWorker(QThread):
                progress = pyqtSignal(str)
                done = pyqtSignal(bool, str)
                def run(self):
                    try:
                        import requests as R
                        s = R.Session()
                        # 忽略系统代理，确保直连本地服务
                        s.trust_env = False
                        no_proxy = {"http": None, "https": None}
                        # 健康检查
                        self.progress.emit("[百应搜索] 开始健康检查 …")
                        healthy = False
                        for i in range(15):
                            try:
                                h = s.get(f"{api_base}/api/health", timeout=0.2, proxies=no_proxy)
                                self.progress.emit(f"[百应搜索] 健康检查 {i+1}/15 -> {h.status_code}")
                                if h.status_code == 200:
                                    healthy = True
                                    break
                            except Exception as e:
                                self.progress.emit(f"[百应搜索] 健康检查错误: {e}")
                            QThread.msleep(200)
                        # 发送请求（带重试）
                        last_err = ""
                        for attempt in range(3):
                            try:
                                self.progress.emit(f"[百应搜索] 发送请求 尝试 {attempt+1}/3 …")
                                r = s.post(api_url, json=payload, timeout=5, proxies=no_proxy)
                                self.progress.emit(f"[百应搜索] 响应状态: {r.status_code}")
                                if r.status_code == 200:
                                    self.done.emit(True, "")
                                    return
                                last_err = f"HTTP {r.status_code}"
                            except Exception as e:
                                last_err = str(e)
                                self.progress.emit(f"[百应搜索] 请求异常: {e}")
                            QThread.msleep(300)
                        self.done.emit(False, last_err or "unknown")
                    except Exception as e:
                        self.done.emit(False, str(e))

            self._search_worker = SearchWorker()
            self._search_worker.progress.connect(lambda m: log(m))
            def on_done(success, err):
                if success:
                    log("✅ [百应搜索] 已发送到浏览器插件", "#4ade80")
                    if hasattr(self, 'search_button') and self.search_button:
                        self.search_button.setText("✓ 已发送")
                        QTimer.singleShot(1000, lambda: self.hide_search_button())
                else:
                    log(f"❌ [百应搜索] 发送失败：{err}", "#f87171")
                    if hasattr(self, 'search_button') and self.search_button:
                        self.search_button.setText("⚠ 服务不可用")
                        QTimer.singleShot(2000, lambda: self._reset_search_button())
            self._search_worker.done.connect(on_done)
            self._search_worker.start()
        except Exception as e:
            log(f"❌ [百应搜索] 本地异常：{e}", "#f87171")
            if hasattr(self, 'search_button') and self.search_button:
                self.search_button.setText("⚠ 发送失败")
                QTimer.singleShot(2000, lambda: self._reset_search_button())

    def _reset_search_button(self):
        """重置搜索按钮到初始状态"""
        if hasattr(self, 'search_button') and self.search_button:
            self.search_button.setText("🔍 巨量百应搜索")
            self.search_button.setEnabled(True)
            # 隐藏按钮而不是保持显示
            self.search_button.hide()
            
    def hide_search_button(self):
        """隐藏搜索按钮"""
        if hasattr(self, 'search_button') and self.search_button:
            self.search_button.hide()

            
    def _fallback_browser_search(self, search_text):
        """备用方案：直接在浏览器中打开巨量百应搜索"""
        try:
            import webbrowser
            import urllib.parse
            # 构建巨量百应搜索URL，使用URL编码确保中文支持
            encoded_text = urllib.parse.quote(search_text)
            search_url = f"https://e.oceanengine.com/compass/keyword/analysis?keyword={encoded_text}"
            print(f"备用方案：打开浏览器搜索: {search_url}")
            webbrowser.open(search_url)
        except Exception as e:
            print(f"打开浏览器搜索失败: {e}")
            
    def copy_product_id(self, product_id):
        """复制商品ID到剪贴板"""
        try:
            # 获取应用程序实例
            app = QApplication.instance()
            if app is None:
                print("无法获取QApplication实例")
                return
                
            # 获取系统剪贴板
            clipboard = QApplication.clipboard()  # 直接使用QApplication.clipboard()
            if clipboard is None:
                print("无法获取系统剪贴板")
                return
                
            # 复制文本到剪贴板，使用多种模式确保兼容性
            clipboard.setText(str(product_id), QClipboard.Mode.Clipboard)
            clipboard.setText(str(product_id), QClipboard.Mode.Selection)
            
            # 强制刷新剪贴板
            app.processEvents()
            
            # 验证复制是否成功
            copied_text = clipboard.text(QClipboard.Mode.Clipboard)
            if copied_text == str(product_id):
                # 简单的视觉反馈
                original_text = self.copy_id_btn.text()
                self.copy_id_btn.setText("✓")
                self.copy_id_btn.setStyleSheet("""
                    QPushButton {
                        background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                            stop:0 #48bb78, stop:1 #38a169);
                        color: white;
                        border: none;
                        border-radius: 8px;
                        font-size: 12px;
                        font-weight: 600;
                        padding: 2px 6px;
                    }
                """)
                # 1.5秒后恢复原样
                QTimer.singleShot(1500, lambda: self.restore_copy_button(original_text))
                print(f"复制成功: {product_id}")
            else:
                print(f"复制验证失败: 预期={product_id}, 实际={copied_text}")
                
        except Exception as e:
            print(f"复制失败: {e}")
            import traceback
            traceback.print_exc()
            
    def restore_copy_button(self, original_text):
        """恢复复制按钮的原始状态"""
        if hasattr(self, 'copy_id_btn'):
            self.copy_id_btn.setText(original_text)
            self.copy_id_btn.setStyleSheet("""
                QPushButton {
                    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                        stop:0 #667eea, stop:1 #5a67d8);
                    color: white;
                    border: none;
                    border-radius: 8px;
                    font-size: 10px;
                    font-weight: 600;
                    padding: 2px 6px;
                }
                QPushButton:hover {
                    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                        stop:0 #5a67d8, stop:1 #4c51bf);
                }
                QPushButton:pressed {
                    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                        stop:0 #4c51bf, stop:1 #434190);
                }
            """)
        
    def open_product_detail(self):
        """打开商品详情页面"""
        try:
            # 获取商品ID
            rel_item_id = self.goods_data.get('relItemId', '')
            if rel_item_id:
                # 创建模态加载提示框
                from PyQt5.QtWidgets import QDialog, QVBoxLayout, QProgressBar, QApplication
                
                self.loading_dialog = QDialog()
                self.loading_dialog.setWindowTitle("正在打开详情页")
                self.loading_dialog.setModal(True)
                self.loading_dialog.resize(300, 100)
                
                layout = QVBoxLayout()
                label = QLabel("正在加载商品详情，请稍候...")
                label.setAlignment(Qt.AlignmentFlag.AlignCenter)
                layout.addWidget(label)
                
                progress_bar = QProgressBar()
                progress_bar.setRange(0, 0)  # Indeterminate progress bar
                layout.addWidget(progress_bar)
                
                self.loading_dialog.setLayout(layout)
                self.loading_dialog.show()
                
                # 处理事件循环以确保对话框显示
                QApplication.processEvents()
                
                # 防止重复点击
                self.image_label.setEnabled(False)
                
                # 检查是否已有详情窗口打开，如果有则关闭
                if hasattr(self, '_detail_window') and self._detail_window:
                    try:
                        self._detail_window.close()
                    except:
                        pass
                
                # 导入商品详情页面模块
                from final_product_detail import ProductDetailWindow
                
                # 使用单例模式确保只有一个商品详情窗口实例
                if not hasattr(GoodsCard, '_shared_detail_window'):
                    GoodsCard._shared_detail_window = None
                    
                # 如果已有窗口实例，先关闭它
                if GoodsCard._shared_detail_window:
                    try:
                        GoodsCard._shared_detail_window.close()
                    except:
                        pass
                
                # 创建新的商品详情窗口
                GoodsCard._shared_detail_window = ProductDetailWindow(rel_item_id, main_window=self.get_main_window())
                self._detail_window = GoodsCard._shared_detail_window
                
                # 设置窗口层级，确保在最前面显示
                self._detail_window.show()
                self._detail_window.raise_()
                self._detail_window.activateWindow()
                
                # 关闭加载提示框
                self.loading_dialog.close()
                self.image_label.setEnabled(True)
            else:
                print("无法获取商品ID")
        except Exception as e:
            print(f"打开商品详情页面失败: {e}")
            try:
                mw = self.get_main_window()
                if mw and hasattr(mw, 'add_log_message'):
                    mw.add_log_message(f"❌ 打开详情失败: {e}", "#f87171")
            except Exception:
                pass
            # 关闭加载提示框
            if hasattr(self, 'loading_dialog'):
                self.loading_dialog.close()
            self.image_label.setEnabled(True)
    
    def mousePressEvent(self, a0):
        """卡片点击事件 - 现在不再打开详情页，只有点击图片才打开"""
        # 不做任何操作，只有点击图片才打开详情页
        pass
            
    def restore_title(self, original_text):
        """恢复原始标题"""
        if hasattr(self, 'title_label') and self.title_label:
            self.title_label.setText(original_text)
            self.title_label.setStyleSheet("""
                QLabel {
                    color: #212529;
                    line-height: 1.6;
                }
            """)
            
    def load_image(self):
        # 从商品数据中获取图片URL
        image_url = self.goods_data.get('itemImgUrl', '')
        if not image_url:
            self.image_label.setText("无图片")
            return
            
        # 检查是否正在加载同一张图片
        if hasattr(self, 'current_loading_url') and self.current_loading_url == image_url:
            return
            
        # 检查缓存
        global _image_cache
        if image_url in _image_cache:
            # 直接使用缓存的图片
            cached_pixmap = _image_cache[image_url]
            # 缩放到标签大小，保持宽高比
            scaled_pixmap = cached_pixmap.scaled(
                self.image_label.width(), 
                self.image_label.height(),
                Qt.AspectRatioMode.KeepAspectRatio,
                Qt.TransformationMode.SmoothTransformation
            )
            self.image_label.setPixmap(scaled_pixmap)
            self.image_label.setText("")
            return
        
        # 检查是否已经有相同URL的加载请求在进行
        global _loading_urls
        if image_url in _loading_urls:
            # 如果已经在加载，等待其完成
            self.image_label.setText("图片加载中...")
            return
            
        # 停止之前的加载线程
        if hasattr(self, 'image_loader') and self.image_loader and self.image_loader.isRunning():
            self.image_loader.terminate()
            self.image_loader.wait(100)  # 等待100ms
            
        # 记录当前加载的URL
        self.current_loading_url = image_url
        _loading_urls.add(image_url)
        
        # 显示占位符文本
        self.image_label.setText("图片加载中...")
        
        # 使用异步线程加载图片，避免阻塞UI线程
        from PyQt5.QtCore import QThread, pyqtSignal
        import urllib.request
        import ssl
        
        class ImageLoader(QThread):
            # 发出原始字节，避免在子线程里创建 QPixmap（可能导致崩溃）
            image_loaded = pyqtSignal(bytes, str)  # bytes, URL
            error_occurred = pyqtSignal(str, str)  # error_msg, URL
            
            def __init__(self, url):
                super().__init__()
                self.url = url
                
            def run(self):
                try:
                    # 创建不验证SSL证书的上下文（仅用于测试环境）
                    context = ssl.create_default_context()
                    context.check_hostname = False
                    context.verify_mode = ssl.CERT_NONE
                    
                    # 使用urllib下载图片数据
                    headers = {
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
                    }
                    req = urllib.request.Request(self.url, headers=headers)
                    with urllib.request.urlopen(req, context=context, timeout=5) as response:  # 减少超时时间
                        image_data = response.read()
                    if image_data:
                        self.image_loaded.emit(image_data, self.url)
                    else:
                        self.error_occurred.emit("图片数据为空", self.url)
                except Exception as e:
                    self.error_occurred.emit(str(e), self.url)
        
        # 创建并启动图片加载线程
        self.image_loader = ImageLoader(image_url)
        self.image_loader.image_loaded.connect(self.on_image_loaded)
        self.image_loader.error_occurred.connect(self.on_image_error)
        self.image_loader.start()
            
    def on_image_loaded(self, image_bytes, url):
        """图片加载完成回调"""
        # 清理加载状态
        global _loading_urls
        if url in _loading_urls:
            _loading_urls.remove(url)
            
        # 检查是否是当前请求的图片
        if hasattr(self, 'current_loading_url') and self.current_loading_url != url:
            return  # 忽略过时的请求
            
        # 清理当前加载状态
        if hasattr(self, 'current_loading_url'):
            delattr(self, 'current_loading_url')
            
        # 在主线程中将字节转换为 QImage/QPixmap
        from PyQt5.QtGui import QImage, QPixmap
        image = QImage()
        image.loadFromData(image_bytes)
        if image.isNull():
            self.image_label.setText("加载失败")
            return
        pixmap = QPixmap.fromImage(image)
        # 缩放到标签大小，保持宽高比
        scaled_pixmap = pixmap.scaled(
            self.image_label.width(), 
            self.image_label.height(),
            Qt.AspectRatioMode.KeepAspectRatio,
            Qt.TransformationMode.SmoothTransformation
        )
        
        # 显示图片
        self.image_label.setPixmap(scaled_pixmap)
        self.image_label.setText("")
        
        # 加入缓存（保存原始大小的图片）
        global _image_cache, _image_cache_max_size
        _image_cache[url] = pixmap
        
        # 检查缓存大小，如果超过限制则清理老的缓存
        if len(_image_cache) > _image_cache_max_size:
            # 删除最老的缓存项目
            oldest_key = next(iter(_image_cache))
            del _image_cache[oldest_key]
        
    def on_image_error(self, error_msg, url):
        """图片加载错误回调"""
        # 清理加载状态
        global _loading_urls
        if url in _loading_urls:
            _loading_urls.remove(url)
            
        # 清理当前加载状态
        if hasattr(self, 'current_loading_url') and self.current_loading_url == url:
            delattr(self, 'current_loading_url')
            
        print(f"加载图片失败: {error_msg}")
        # 显示错误信息
        self.image_label.setText("加载失败")

# 数据库配置类
class DatabaseConfig:
    def __init__(self, host=None, port=None, database=None, username=None, password=None):
        # 从配置文件读取数据库配置，如果没有则使用默认值
        self.host = host or config.get("database.host", "localhost")
        self.port = port or config.get("database.port", 3306)
        self.database = database or config.get("database.database", "ssm")
        self.username = username or config.get("database.username", "root")

        # 支持密码为明文或密文；优先使用传入参数，其次使用配置文件
        raw_password = password if password is not None else config.get("database.password")
        self.password = decrypt_password(raw_password)
        
    def to_dict(self):
        return {
            "host": self.host,
            "port": self.port,
            "database": self.database,
            "username": self.username,
            "password": self.password
        }
    
    def save_to_config(self):
        """保存当前配置到配置文件"""
        config.set("database.host", self.host)
        config.set("database.port", self.port)
        config.set("database.database", self.database)
        config.set("database.username", self.username)
        config.set("database.password", self.password)

# 添加全局变量来控制调试日志
DEBUG_LOGGING_ENABLED = False

# Cookie配置对话框
class CookieConfigDialog(QDialog):
    def __init__(self, api_instance, parent=None):
        super().__init__(parent)
        self.api_instance = api_instance
        self.setWindowTitle("Cookie配置")
        self.setModal(True)
        self.resize(700, 500)
        self.setup_ui()
        
    def setup_ui(self):
        # 设置对话框样式
        self.setStyleSheet("""
            QDialog {
                background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif;
            }
            QGroupBox {
                font-weight: bold;
                border: 1px solid #d0d0d0;
                border-radius: 8px;
                margin-top: 1ex;
                padding-top: 10px;
                background-color: white;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px 0 5px;
                color: #333;
            }
            QLabel {
                color: #555;
                font-size: 13px;
            }
            QTextEdit {
                padding: 8px;
                border: 1px solid #ccc;
                border-radius: 4px;
                background-color: white;
                font-family: 'Courier New', monospace;
                font-size: 11px;
            }
            QTextEdit:focus {
                border: 1px solid #0078d4;
            }
            QPushButton {
                padding: 8px 16px;
                border: 1px solid #0078d4;
                border-radius: 4px;
                background-color: #0078d4;
                color: white;
                font-weight: bold;
                min-width: 80px;
            }
            QPushButton:hover {
                background-color: #106ebe;
                border-color: #106ebe;
            }
            QPushButton:pressed {
                background-color: #005a9e;
                border-color: #005a9e;
            }
            QPushButton#secondary {
                background-color: #f3f3f3;
                color: #333;
                border: 1px solid #ccc;
            }
            QPushButton#secondary:hover {
                background-color: #e6e6e6;
                border-color: #999;
            }
            QPushButton#danger {
                background-color: #dc3545;
                border: 1px solid #dc3545;
            }
            QPushButton#danger:hover {
                background-color: #c82333;
                border-color: #c82333;
            }
        """)
        
        layout = QVBoxLayout(self)
        layout.setSpacing(15)
        
        # Cookie配置区域
        cookie_group = QGroupBox("Cookie配置 (支持最多3个Cookie轮换)")
        cookie_layout = QVBoxLayout()
        
        # 操作按钮区域（放在Cookie输入框上方）
        button_area = QHBoxLayout()
        
        # 清除按钮
        clear_btn = QPushButton("清除所有Cookie")
        clear_btn.setObjectName("danger")
        clear_btn.clicked.connect(self.clear_all_cookies)
        button_area.addWidget(clear_btn)
        
        # 复制按钮
        copy_btn = QPushButton("复制第一个到剪贴板")
        copy_btn.setObjectName("secondary")
        copy_btn.clicked.connect(self.copy_first_cookie)
        button_area.addWidget(copy_btn)
        
        button_area.addStretch()
        cookie_layout.addLayout(button_area)
        
        # Cookie输入框 - 快手CPS商品榜
        self.cookie_editors = []
        cps_title = QLabel("快手CPS商品榜 Cookie（最多3个轮询）")
        cps_title.setStyleSheet("font-weight:600; color:#111827;")
        cookie_layout.addWidget(cps_title)
        for i in range(3):
            label = QLabel(f"商品Cookie {i+1}:")
            cookie_layout.addWidget(label)
            
            editor = QTextEdit()
            editor.setMaximumHeight(80)
            editor.setPlaceholderText(f"请输入第{i+1}个快手CPS商品榜 Cookie 字符串...")
            cookie_layout.addWidget(editor)
            self.cookie_editors.append(editor)

        # 磁力金牛素材榜 Cookie 区域
        material_group = QGroupBox("磁力金牛素材榜 Cookie（建议配置 1~2 个用于轮询）")
        material_layout = QVBoxLayout(material_group)
        self.material_cookie_editors = []
        for i in range(2):
            m_label = QLabel(f"金牛Cookie {i+1}:")
            material_layout.addWidget(m_label)
            m_editor = QTextEdit()
            m_editor.setMaximumHeight(80)
            m_editor.setPlaceholderText(f"请输入第{i+1}个磁力金牛 Cookie 字符串...")
            self.material_cookie_editors.append(m_editor)
            material_layout.addWidget(m_editor)
        cookie_layout.addWidget(material_group)
        
        # 从配置文件或API实例加载现有Cookie
        self.load_existing_cookies()
        
        cookie_group.setLayout(cookie_layout)
        layout.addWidget(cookie_group)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        button_layout.setSpacing(10)
        
        save_btn = QPushButton("保存配置")
        save_btn.clicked.connect(self.accept)
        save_btn.setMinimumWidth(100)
        
        cancel_btn = QPushButton("取消")
        cancel_btn.setObjectName("secondary")
        cancel_btn.clicked.connect(self.reject)
        cancel_btn.setMinimumWidth(100)
        
        button_layout.addStretch()
        button_layout.addWidget(save_btn)
        button_layout.addWidget(cancel_btn)
        layout.addLayout(button_layout)
        
    def load_existing_cookies(self):
        """加载现有的Cookie配置"""
        # 快手CPS商品榜 Cookie（来自 API 实例或配置文件）
        if hasattr(self.api_instance, 'cookies') and self.api_instance.cookies:
            for i, cookie in enumerate(self.api_instance.cookies[:3]):
                if i < len(self.cookie_editors):
                    self.cookie_editors[i].setPlainText(cookie)
        else:
            try:
                from config import config as app_config
                saved_cps = app_config.get("cookies.list", []) or []
                for i, cookie in enumerate(saved_cps[:3]):
                    if i < len(self.cookie_editors):
                        self.cookie_editors[i].setPlainText(cookie)
            except Exception:
                pass
        
        # 磁力金牛素材榜 Cookie（只从配置文件读取）
        try:
            from config import config as app_config
            saved_material = app_config.get("material_cookies.list", []) or []
            for i, cookie in enumerate(saved_material[:2]):
                if i < len(self.material_cookie_editors):
                    self.material_cookie_editors[i].setPlainText(cookie)
        except Exception:
            pass
        
    def clear_all_cookies(self):
        """清除所有Cookie输入框"""
        reply = QMessageBox.question(
            self, '确认清除', 
            '确定要清除所有Cookie（包括商品榜与素材榜）吗？',
            QMessageBox.Yes | QMessageBox.No
        )
        if reply == QMessageBox.Yes:
            for editor in self.cookie_editors:
                editor.clear()
            if hasattr(self, 'material_cookie_editors'):
                for editor in self.material_cookie_editors:
                    editor.clear()
                
    def copy_first_cookie(self):
        """复制第一个Cookie到剪贴板"""
        first_cookie = self.cookie_editors[0].toPlainText().strip()
        if first_cookie:
            # 获取应用程序实例
            app = QApplication.instance()
            if app is not None:
                clipboard = QApplication.clipboard()  # 直接使用QApplication.clipboard()
                if clipboard is not None:
                    clipboard.setText(first_cookie)
                    QMessageBox.information(self, '复制成功', '第一个Cookie已复制到剪贴板')
                else:
                    QMessageBox.warning(self, '复制失败', '无法访问系统剪贴板')
            else:
                QMessageBox.warning(self, '复制失败', '无法获取应用程序实例')
        else:
            QMessageBox.warning(self, '复制失败', '第一个Cookie为空')
            
    def get_cookies(self):
        """获取配置的商品榜 Cookie 列表"""
        cookies = []
        for editor in self.cookie_editors:
            cookie = editor.toPlainText().strip()
            if cookie:
                cookies.append(cookie)
        return cookies

    def get_material_cookies(self):
        """获取配置的磁力金牛素材榜 Cookie 列表"""
        material_cookies = []
        if hasattr(self, 'material_cookie_editors'):
            for editor in self.material_cookie_editors:
                cookie = editor.toPlainText().strip()
                if cookie:
                    material_cookies.append(cookie)
        return material_cookies
        
    def save_cookies(self):
        """保存Cookie配置"""
        cookies = self.get_cookies()
        material_cookies = self.get_material_cookies()
        if cookies:
            # 更新API实例的Cookie配置（仅商品榜）
            self.api_instance.update_cookies(cookies)
            
            # 保存到配置文件
            from config import config as app_config
            app_config.set("cookies.list", cookies)
            # 磁力金牛素材榜 Cookie 单独保存
            app_config.set("material_cookies.list", material_cookies)
            
            return True
        else:
            QMessageBox.warning(self, '配置错误', '至少需要设置一个商品榜 Cookie')
            return False

# 数据库配置对话框
class DatabaseConfigDialog(QDialog):
    def __init__(self, db_config, parent=None):
        super().__init__(parent)
        self.db_config = db_config
        self.setWindowTitle("数据库配置")
        self.setModal(True)
        self.resize(400, 200)
        self.setup_ui()
        
    def setup_ui(self):
        layout = QFormLayout(self)
        
        self.host_edit = QLineEdit(self.db_config.host)
        layout.addRow("主机地址:", self.host_edit)
        
        self.port_edit = QLineEdit(str(self.db_config.port))
        layout.addRow("端口:", self.port_edit)
        
        self.db_edit = QLineEdit(self.db_config.database)
        layout.addRow("数据库名:", self.db_edit)
        
        self.user_edit = QLineEdit(self.db_config.username)
        layout.addRow("用户名:", self.user_edit)
        
        self.pass_edit = QLineEdit(self.db_config.password)
        self.pass_edit.setEchoMode(QLineEdit.Password)
        layout.addRow("密码:", self.pass_edit)
        
        # 按钮
        button_layout = QHBoxLayout()
        save_btn = QPushButton("保存")
        save_btn.clicked.connect(self.accept)
        cancel_btn = QPushButton("取消")
        cancel_btn.clicked.connect(self.reject)
        button_layout.addWidget(save_btn)
        button_layout.addWidget(cancel_btn)
        layout.addRow(button_layout)
        
    def get_config(self):
        # 创建新的数据库配置对象
        new_config = DatabaseConfig(
            host=self.host_edit.text(),
            port=int(self.port_edit.text()),
            database=self.db_edit.text(),
            username=self.user_edit.text(),
            password=self.pass_edit.text()
        )
        # 保存到配置文件
        new_config.save_to_config()
        return new_config

# 数据抓取配置对话框
class CrawlConfigDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("数据抓取配置")
        self.setModal(True)
        self.resize(600, 600)  # 调整窗口大小以适应延时配置
        self.setup_ui()
        
    def setup_ui(self):
        # 设置对话框样式
        self.setStyleSheet("""
            QDialog {
                background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif;
            }
            QGroupBox {
                font-weight: bold;
                border: 1px solid #d0d0d0;
                border-radius: 8px;
                margin-top: 1ex;
                padding-top: 10px;
                background-color: white;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px 0 5px;
                color: #333;
            }
            QLabel {
                color: #555;
                font-size: 13px;
            }
            QCheckBox {
                spacing: 5px;
                padding: 8px;
                border-radius: 4px;
            }
            QCheckBox::indicator {
                width: 18px;
                height: 18px;
            }
            QCheckBox::indicator:unchecked {
                border: 1px solid #ccc;
                border-radius: 3px;
                background-color: white;
            }
            QCheckBox::indicator:checked {
                border: 1px solid #0078d4;
                border-radius: 3px;
                background-color: #0078d4;
            }
            QCheckBox::indicator:checked {
                image: none;
            }
            QCheckBox:hover {
                background-color: #f0f8ff;
            }
            QComboBox {
                padding: 8px;
                border: 1px solid #ccc;
                border-radius: 4px;
                background-color: white;
                min-width: 100px;
            }
            QComboBox:focus {
                border: 1px solid #0078d4;
            }
            QComboBox::drop-down {
                subcontrol-origin: padding;
                subcontrol-position: top right;
                width: 20px;
                border-left: 1px solid #ccc;
            }
            QPushButton {
                padding: 8px 16px;
                border: 1px solid #0078d4;
                border-radius: 4px;
                background-color: #0078d4;
                color: white;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #106ebe;
                border-color: #106ebe;
            }
            QPushButton:pressed {
                background-color: #005a9e;
                border-color: #005a9e;
            }
            QPushButton#secondary {
                background-color: #f3f3f3;
                color: #333;
                border: 1px solid #ccc;
            }
            QPushButton#secondary:hover {
                background-color: #e6e6e6;
                border-color: #999;
            }
        """)
        
        layout = QFormLayout(self)
        layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
        layout.setHorizontalSpacing(15)
        layout.setVerticalSpacing(12)
        
        # 类目配置 - 使用多选框替代文本输入框
        self.category_group = QGroupBox("选择要抓取的类目（默认全选）")
        category_layout = QVBoxLayout()
        
        # 定义十二个大类目
        self.specified_categories = [
            ("食品饮料", "食品饮料"),
            ("家居百货", "家居百货"),
            ("女装女鞋", "女装女鞋"),
            ("美妆护肤", "美妆护肤"),
            ("个护清洁", "个护清洁"),
            ("医疗保健", "医疗保健"),
            ("母婴玩具", "母婴玩具"),
            ("茶酒生鲜", "茶酒生鲜"),
            ("男装男鞋", "男装男鞋"),
            ("运动户外", "运动户外"),
            ("数码家电", "数码家电"),
            ("珠宝配饰", "珠宝配饰")
        ]
        
        # 创建复选框列表，使用网格布局实现横向排列
        category_grid_layout = QGridLayout()
        row = 0
        col = 0
        self.category_checkboxes = []
        
        # 从配置文件加载上次保存的类目选择
        from config import config as app_config
        saved_categories = app_config.get("crawl.selected_categories", [])
        
        for category_name, category_display in self.specified_categories:
            checkbox = QCheckBox(category_display)
            # 如果配置文件中有保存的选择，则使用保存的状态；否则默认勾选
            if saved_categories:
                checkbox.setChecked(category_name in saved_categories)
            else:
                checkbox.setChecked(True)  # 默认勾选
            category_grid_layout.addWidget(checkbox, row, col)
            self.category_checkboxes.append((category_name, checkbox))
            
            # 每行显示4个复选框
            col += 1
            if col >= 4:
                col = 0
                row += 1
        
        category_layout.addLayout(category_grid_layout)
        self.category_group.setLayout(category_layout)
        layout.addRow(self.category_group)
        
        # 日期范围
        self.date_range_combo = QComboBox()
        self.date_range_combo.addItems(["1h", "1d", "7d", "30d"])
        # 从配置文件加载上次设置
        saved_date_range = app_config.get("crawl.date_range", "30d")
        # 确保是字符串类型
        if isinstance(saved_date_range, dict):
            saved_date_range = saved_date_range.get("value", "30d")
        self.date_range_combo.setCurrentText(str(saved_date_range))
        layout.addRow("日期范围:", self.date_range_combo)
        
        # 榜单类型
        self.rank_type_combo = QComboBox()
        self.rank_type_combo.addItems(["total", "live", "video", "wind"])
        # 从配置文件加载上次设置
        saved_rank_type = app_config.get("crawl.rank_type", "video")
        # 确保是字符串类型
        if isinstance(saved_rank_type, dict):
            saved_rank_type = saved_rank_type.get("value", "video")
        self.rank_type_combo.setCurrentText(str(saved_rank_type))
        layout.addRow("榜单类型:", self.rank_type_combo)
        
        # 延时配置区域
        self.delay_group = QGroupBox("延时配置")
        delay_layout = QFormLayout()
        
        # 从配置文件读取当前值
        from config import config as app_config
        
        # 获取配置值并确保是整数类型
        def get_delay_value(key, default):
            value = app_config.get(key, default)
            # 确保返回整数
            if isinstance(value, dict):
                dict_value = value.get("value", default)
                if dict_value is None:
                    return default
                try:
                    return int(dict_value)
                except (ValueError, TypeError):
                    return default
            if value is None:
                return default
            try:
                return int(value)
            except (ValueError, TypeError):
                return default
        
        # 类目间延迟
        self.category_delay_spin = QSpinBox()
        self.category_delay_spin.setRange(0, 60)
        self.category_delay_spin.setSuffix(" 秒")
        self.category_delay_spin.setValue(get_delay_value("delay.category_delay", 3))
        delay_layout.addRow("类目间延迟:", self.category_delay_spin)
        
        # 子类目间延迟
        self.subcategory_delay_spin = QSpinBox()
        self.subcategory_delay_spin.setRange(0, 60)
        self.subcategory_delay_spin.setSuffix(" 秒")
        self.subcategory_delay_spin.setValue(get_delay_value("delay.subcategory_delay", 2))
        delay_layout.addRow("子类目间延迟:", self.subcategory_delay_spin)
        
        # 商品处理延迟
        self.goods_delay_spin = QSpinBox()
        self.goods_delay_spin.setRange(0, 60)
        self.goods_delay_spin.setSuffix(" 秒")
        self.goods_delay_spin.setValue(get_delay_value("delay.goods_delay", 1))
        delay_layout.addRow("商品处理延迟:", self.goods_delay_spin)
        
        # 商品视频请求延迟
        self.product_video_delay_spin = QSpinBox()
        self.product_video_delay_spin.setRange(0, 60)
        self.product_video_delay_spin.setSuffix(" 秒")
        self.product_video_delay_spin.setValue(get_delay_value("delay.product_video_delay", 1))
        delay_layout.addRow("商品视频请求延迟:", self.product_video_delay_spin)
        
        # 重试延迟
        self.retry_delay_spin = QSpinBox()
        self.retry_delay_spin.setRange(0, 60)
        self.retry_delay_spin.setSuffix(" 秒")
        self.retry_delay_spin.setValue(get_delay_value("delay.retry_delay", 5))
        delay_layout.addRow("重试延迟:", self.retry_delay_spin)
        
        self.delay_group.setLayout(delay_layout)
        layout.addRow(self.delay_group)
        
        # 数据清除配置区域
        self.cleanup_group = QGroupBox("数据清除配置")
        cleanup_layout = QFormLayout()
        
        # 数据清除选项
        self.cleanup_combo = QComboBox()
        self.cleanup_combo.addItems([
            "清除24小时之前的数据",
            "清除3天之前的数据", 
            "清除1周之前的数据",
            "清除所有数据",
            "不清除数据"
        ])
        # 从配置文件加载上次设置
        saved_cleanup_option = app_config.get("crawl.cleanup_option", "清除24小时之前的数据")
        if isinstance(saved_cleanup_option, str):
            self.cleanup_combo.setCurrentText(saved_cleanup_option)
        else:
            self.cleanup_combo.setCurrentText("清除24小时之前的数据")
        self.cleanup_combo.setStyleSheet("""
            QComboBox {
                padding: 8px;
                border: 1px solid #ccc;
                border-radius: 4px;
                background-color: white;
                min-width: 200px;
            }
            QComboBox:focus {
                border: 1px solid #0078d4;
            }
        """)
        
        cleanup_layout.addRow("数据清除:", self.cleanup_combo)
        
        # 清除说明
        cleanup_desc = QLabel("• 数据清除将在数据抓取启动后异步执行\n• 清除操作为物理删除，数据无法恢复\n• 请谨慎选择清除范围")
        cleanup_desc.setStyleSheet("""
            QLabel {
                color: #666;
                font-size: 11px;
                padding: 5px;
                background-color: #fff3cd;
                border: 1px solid #ffeaa7;
                border-radius: 4px;
                line-height: 1.4;
            }
        """)
        cleanup_layout.addRow(cleanup_desc)
        
        self.cleanup_group.setLayout(cleanup_layout)
        layout.addRow(self.cleanup_group)
        
        # 按钮
        button_layout = QHBoxLayout()
        button_layout.setSpacing(10)
        
        start_btn = QPushButton("开始抓取")
        start_btn.clicked.connect(self.accept)
        start_btn.setMinimumWidth(100)
        cancel_btn = QPushButton("取消")
        cancel_btn.setObjectName("secondary")
        cancel_btn.clicked.connect(self.reject)
        cancel_btn.setMinimumWidth(100)
        button_layout.addStretch()
        button_layout.addWidget(start_btn)
        button_layout.addWidget(cancel_btn)
        layout.addRow(button_layout)
        
    def get_config(self):
        # 获取选中的类目
        selected_categories = []
        for category_name, checkbox in self.category_checkboxes:
            if checkbox.isChecked():
                selected_categories.append(category_name)
        
        # 保存所有配置到配置文件
        from config import config as app_config
        app_config.set("crawl.selected_categories", selected_categories)
        app_config.set("crawl.date_range", self.date_range_combo.currentText())
        app_config.set("crawl.rank_type", self.rank_type_combo.currentText())
        app_config.set("crawl.cleanup_option", self.cleanup_combo.currentText())
        
        # 保存延时配置到配置文件
        delay_config = {
            "delay.category_delay": self.category_delay_spin.value(),
            "delay.subcategory_delay": self.subcategory_delay_spin.value(),
            "delay.goods_delay": self.goods_delay_spin.value(),
            "delay.product_video_delay": self.product_video_delay_spin.value(),
            "delay.retry_delay": self.retry_delay_spin.value()
        }
        for key, value in delay_config.items():
            app_config.set(key, value)
            
        return {
            "selected_categories": selected_categories if selected_categories else None,
            "date_range": self.date_range_combo.currentText(),
            "rank_type": self.rank_type_combo.currentText(),
            "delay_config": delay_config,
            "cleanup_option": self.cleanup_combo.currentText()
        }

# 视频下载配置对话框
class DownloadConfigDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.download_path = ""
        self.setWindowTitle("视频下载配置")
        self.setModal(True)
        self.resize(500, 250)
        self.setup_ui()
        
    def setup_ui(self):
        # 设置对话框样式
        self.setStyleSheet("""
            QDialog {
                background-color: #f5f5f5;
                font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif;
            }
            QGroupBox {
                font-weight: bold;
                border: 1px solid #d0d0d0;
                border-radius: 8px;
                margin-top: 1ex;
                padding-top: 10px;
                background-color: white;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px 0 5px;
                color: #333;
            }
            QLabel {
                color: #555;
                font-size: 13px;
            }
            QLineEdit {
                padding: 8px;
                border: 1px solid #ccc;
                border-radius: 4px;
                background-color: white;
                selection-background-color: #0078d4;
            }
            QLineEdit:focus {
                border: 1px solid #0078d4;
            }
            QPushButton {
                padding: 8px 16px;
                border: 1px solid #0078d4;
                border-radius: 4px;
                background-color: #0078d4;
                color: white;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #106ebe;
                border-color: #106ebe;
            }
            QPushButton:pressed {
                background-color: #005a9e;
                border-color: #005a9e;
            }
            QPushButton#secondary {
                background-color: #f3f3f3;
                color: #333;
                border: 1px solid #ccc;
            }
            QPushButton#secondary:hover {
                background-color: #e6e6e6;
                border-color: #999;
            }
            QSpinBox {
                padding: 6px;
                border: 1px solid #ccc;
                border-radius: 4px;
                background-color: white;
                min-width: 80px;
            }
            QSpinBox::up-button, QSpinBox::down-button {
                width: 16px;
            }
            QSpinBox:focus {
                border: 1px solid #0078d4;
            }
        """)
        
        layout = QFormLayout(self)
        layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
        layout.setHorizontalSpacing(15)
        layout.setVerticalSpacing(12)
        
        # 下载路径
        path_layout = QHBoxLayout()
        self.path_edit = QLineEdit()
        # 从配置文件加载上次设置的路径
        from config import config as app_config
        saved_path = app_config.get("download.default_path", "ks-download")
        if isinstance(saved_path, str) and saved_path != "ks-download":
            # 如果保存的路径不是默认值，则使用保存的路径
            self.download_path = saved_path
        else:
            # 设置默认下载路径为当前程序目录下的ks-download文件夹
            import os
            default_download_path = os.path.join(os.getcwd(), "ks-download")
            self.download_path = default_download_path
        self.path_edit.setText(self.download_path)
        self.path_edit.setReadOnly(True)
        path_btn = QPushButton("选择文件夹")
        path_btn.setObjectName("secondary")
        path_btn.clicked.connect(self.select_path)
        path_layout.addWidget(self.path_edit)
        path_layout.addWidget(path_btn)
        layout.addRow("下载路径:", path_layout)
        
        # 线程数
        self.thread_spin = QSpinBox()
        self.thread_spin.setRange(1, 10)
        # 从配置文件加载上次设置
        saved_threads = app_config.get("download.max_threads", 3)
        if isinstance(saved_threads, int):
            self.thread_spin.setValue(saved_threads)
        else:
            self.thread_spin.setValue(3)
        self.thread_spin.setSuffix(" 个线程")
        layout.addRow("线程数:", self.thread_spin)
        
        # 视频请求延迟
        self.video_request_delay_spin = QSpinBox()
        self.video_request_delay_spin.setRange(0, 60)
        self.video_request_delay_spin.setSuffix(" 秒")
        # 从配置文件加载上次设置
        saved_video_delay = app_config.get("download.video_request_delay", 15)
        if isinstance(saved_video_delay, int):
            self.video_request_delay_spin.setValue(saved_video_delay)
        else:
            self.video_request_delay_spin.setValue(15)
        layout.addRow("视频请求延迟:", self.video_request_delay_spin)
        
        # 开启子类目选项
        self.enable_subcategory_checkbox = QCheckBox("开启子类目(按 类目/子类目/佣金段 分类)")
        from config import config as app_config
        saved_enable_sub = app_config.get("download.enable_subcategory", False)
        if isinstance(saved_enable_sub, dict):
            saved_enable_sub = saved_enable_sub.get("enabled", False) or saved_enable_sub.get("value", False)
        self.enable_subcategory_checkbox.setChecked(bool(saved_enable_sub))
        layout.addRow("目录组织:", self.enable_subcategory_checkbox)

        # 自动下载
        auto_group = QHBoxLayout()
        self.auto_download_checkbox = QCheckBox("开启自动下载")
        saved_auto = app_config.get("download.auto.enabled", False)
        if isinstance(saved_auto, dict):
            saved_auto = saved_auto.get("value", False)
        self.auto_download_checkbox.setChecked(bool(saved_auto))
        self.auto_interval_combo = QComboBox()
        self.auto_interval_combo.addItems(["10分钟", "30分钟", "1小时", "2小时"])
        saved_minutes = app_config.get("download.auto.interval_minutes", 0)
        try:
            saved_minutes = int(saved_minutes if not isinstance(saved_minutes, dict) else saved_minutes.get("value", 0))
        except Exception:
            saved_minutes = 0
        # 设置默认选项
        minutes_to_index = {10:0, 30:1, 60:2, 120:3}
        if saved_minutes in minutes_to_index:
            self.auto_interval_combo.setCurrentIndex(minutes_to_index[saved_minutes])
        auto_group.addWidget(self.auto_download_checkbox)
        auto_group.addWidget(self.auto_interval_combo)
        auto_group.addStretch()
        layout.addRow("自动下载:", auto_group)

        # 按钮
        button_layout = QHBoxLayout()
        button_layout.setSpacing(10)
        start_btn = QPushButton("开始下载")
        start_btn.clicked.connect(self.accept)
        start_btn.setMinimumWidth(100)
        cancel_btn = QPushButton("取消")
        cancel_btn.setObjectName("secondary")
        cancel_btn.clicked.connect(self.reject)
        cancel_btn.setMinimumWidth(100)
        button_layout.addStretch()
        button_layout.addWidget(start_btn)
        button_layout.addWidget(cancel_btn)
        layout.addRow(button_layout)
        
    def select_path(self):
        path = QFileDialog.getExistingDirectory(self, "选择下载文件夹")
        if path:
            self.path_edit.setText(path)
            self.download_path = path
            
    def get_config(self):
        # 保存下载配置到配置文件
        from config import config as app_config
        app_config.set("download.default_path", self.download_path)
        app_config.set("download.max_threads", self.thread_spin.value())
        app_config.set("download.video_request_delay", self.video_request_delay_spin.value())
        app_config.set("download.enable_subcategory", self.enable_subcategory_checkbox.isChecked())
        # 保存自动下载配置
        minutes_map = {"10分钟":10, "30分钟":30, "1小时":60, "2小时":120}
        app_config.set("download.auto.enabled", self.auto_download_checkbox.isChecked())
        app_config.set("download.auto.interval_minutes", minutes_map.get(self.auto_interval_combo.currentText(), 0))
        
        return {
            "download_path": self.download_path,
            "thread_count": self.thread_spin.value(),
            "video_request_delay": self.video_request_delay_spin.value(),
            "enable_subcategory": self.enable_subcategory_checkbox.isChecked(),
            "auto": {
                "enabled": self.auto_download_checkbox.isChecked(),
                "interval_minutes": minutes_map.get(self.auto_interval_combo.currentText(), 0)
            }
        }

# 面板：数据抓取配置（用于Tab，不弹窗）
class CrawlConfigPanel(QWidget):
    def __init__(self, main_window):
        super().__init__(main_window)
        self.main_window = main_window
        # 运行期统计，防止界面数值回退到更小值（例如偶发解析为0导致闪回）
        self._goods_count = 0
        self._video_count = 0
        # 今日累计基数（当天已累计的数量，不含“本次”正在抓取的数量）
        self._today_base_goods = 0
        self._today_base_videos = 0
        self._load_today_base_counts()
        self.setup_ui()

    def on_counts_updated(self, goods: int, videos: int):
        # 仅向上更新，避免回退
        try:
            if goods < self._goods_count or videos < self._video_count:
                return
            self._goods_count, self._video_count = goods, videos
            # 统一通过辅助方法更新，防止覆盖“今日累计”
            self._update_stats_label_text()
        except Exception:
            pass

    def _load_today_base_counts(self):
        """加载并校正当日累计基数（如果跨天则清零）"""
        try:
            from datetime import datetime
            from config import config as app_config
            today = datetime.now().strftime('%Y%m%d')
            stats_date = app_config.get("stats.crawl.date", "")
            goods_sum = app_config.get("stats.crawl.goods", 0)
            videos_sum = app_config.get("stats.crawl.videos", 0)
            
            # 兼容旧版本：如果date字段不存在或为空，自动添加并清零
            if not stats_date or stats_date != today:
                goods_sum, videos_sum = 0, 0
                app_config.set("stats.crawl.date", today)
                app_config.set("stats.crawl.goods", 0)
                app_config.set("stats.crawl.videos", 0)
            
            # 兜底转换为整数
            try:
                goods_sum = int(goods_sum)
            except Exception:
                goods_sum = 0
            try:
                videos_sum = int(videos_sum)
            except Exception:
                videos_sum = 0
            self._today_base_goods = max(0, goods_sum)
            self._today_base_videos = max(0, videos_sum)
        except Exception:
            self._today_base_goods = 0
            self._today_base_videos = 0

    def _update_stats_label_text(self):
        """根据当前与当日累计更新界面文案"""
        try:
            total_goods_today = self._today_base_goods + self._goods_count
            total_videos_today = self._today_base_videos + self._video_count
            self.stats_label.setText(
                f"已保存商品总数：{self._goods_count}，视频 url 数：{self._video_count} ｜ 今日累计：商品 {total_goods_today}，视频 {total_videos_today}"
            )
            # 立即刷新
            from PyQt5.QtWidgets import QApplication
            QApplication.processEvents()
        except Exception:
            pass

    def setup_ui(self):
        # 美化样式
        self.setStyleSheet("""
            QWidget { font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif; }
            QGroupBox { font-weight: bold; border: 1px solid #e5e7eb; border-radius: 10px; margin-top: 8px; padding-top: 10px; background: #ffffff; }
            QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 6px; color: #374151; }
            QLabel { color: #374151; font-size: 13px; }
            QCheckBox { spacing: 4px; padding: 4px 6px; border-radius: 6px; }
            QCheckBox::indicator { width: 16px; height: 16px; border: 1px solid #94a3b8; border-radius: 3px; background: #fff; }
            QCheckBox::indicator:checked { background-color: #2563eb; border-color: #2563eb; }
            QCheckBox:checked { background-color: #e0f2fe; color: #1e40af; border: 1px solid #60a5fa; }
            QCheckBox:hover { background-color: #f3f4f6; }
            QComboBox, QSpinBox, QLineEdit {
                padding: 6px 10px; border: 1px solid #e5e7eb; border-radius: 8px; background-color: white;
                min-width: 110px; min-height: 28px; font-size: 13px;
            }
            QAbstractSpinBox { padding-right: 26px; }
            QSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; height: 13px; }
            QSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; height: 13px; }
            QComboBox:focus, QSpinBox:focus, QLineEdit:focus { border: 1px solid #3b82f6; }
            QPushButton { padding: 8px 14px; border: none; border-radius: 8px; background-color: #2563eb; color: white; font-weight: 600; }
            QPushButton:hover { background-color: #1d4ed8; }
            QPushButton#secondary { background-color: #f3f4f6; color: #111827; }
            QPushButton#secondary:hover { background-color: #e5e7eb; }
        """)
        
        outer = QVBoxLayout(self)
        outer.setSpacing(8)
        outer.setContentsMargins(4, 4, 4, 4)
        # 顶部标题移除
        layout = QFormLayout()
        layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
        layout.setHorizontalSpacing(14)
        layout.setVerticalSpacing(10)
        outer.addLayout(layout)

        # 类目多选
        self.category_group = QGroupBox("")
        category_layout = QVBoxLayout()
        self.specified_categories = [
            ("食品饮料", "食品饮料"),
            ("家居百货", "家居百货"),
            ("女装女鞋", "女装女鞋"),
            ("美妆护肤", "美妆护肤"),
            ("个护清洁", "个护清洁"),
            ("医疗保健", "医疗保健"),
            ("母婴玩具", "母婴玩具"),
            ("茶酒生鲜", "茶酒生鲜"),
            ("男装男鞋", "男装男鞋"),
            ("运动户外", "运动户外"),
            ("数码家电", "数码家电"),
            ("珠宝配饰", "珠宝配饰")
        ]
        grid = QGridLayout()
        self.category_checkboxes = []
        from config import config as app_config
        saved_categories = app_config.get("crawl.selected_categories", [])
        row = col = 0
        for category_name, category_display in self.specified_categories:
            cb = QCheckBox(category_display)
            cb.setChecked(category_name in saved_categories or not saved_categories)
            grid.addWidget(cb, row, col)
            self.category_checkboxes.append((category_name, cb))
            col += 1
            if col >= 6:
                col = 0
                row += 1
        category_layout.addLayout(grid)
        self.category_group.setLayout(category_layout)
        layout.addRow(self.category_group)

        # 日期范围 + 榜单类型 同行显示
        self.date_range_combo = QComboBox()
        self.date_range_combo.addItems(["1h", "1d", "7d", "30d"])
        saved_date_range = app_config.get("crawl.date_range", "30d")
        saved_date_range = saved_date_range.get("value", "30d") if isinstance(saved_date_range, dict) else saved_date_range
        self.date_range_combo.setCurrentText(str(saved_date_range))

        self.rank_type_combo = QComboBox()
        self.rank_type_combo.addItems(["total", "live", "video", "wind"])
        saved_rank_type = app_config.get("crawl.rank_type", "video")
        saved_rank_type = saved_rank_type.get("value", "video") if isinstance(saved_rank_type, dict) else saved_rank_type
        self.rank_type_combo.setCurrentText(str(saved_rank_type))

        # 数据清除（先定义供顶部行使用）
        self.cleanup_combo = QComboBox()
        self.cleanup_combo.addItems(["清除24小时之前的数据", "清除3天之前的数据", "清除1周之前的数据", "清除所有数据", "不清除数据"])
        saved_cleanup_option = app_config.get("crawl.cleanup_option", "清除24小时之前的数据")
        if isinstance(saved_cleanup_option, str):
            self.cleanup_combo.setCurrentText(saved_cleanup_option)

        # 自动抓取（先定义后用于顶部行）
        saved_auto = app_config.get("crawl.auto.enabled", False)
        if isinstance(saved_auto, dict):
            saved_auto = saved_auto.get("value", False)
        self.auto_crawl_checkbox = QCheckBox("开启自动抓取")
        self.auto_crawl_checkbox.setChecked(bool(saved_auto))
        self.auto_crawl_interval = QComboBox()
        self.auto_crawl_interval.addItems(["30分钟", "1小时", "2小时", "4小时"])
        saved_minutes = app_config.get("crawl.auto.interval_minutes", 0)
        try:
            saved_minutes = int(saved_minutes if not isinstance(saved_minutes, dict) else saved_minutes.get("value", 0))
        except Exception:
            saved_minutes = 0
        minutes_map = {30:0, 60:1, 120:2, 240:3}
        if saved_minutes in minutes_map:
            self.auto_crawl_interval.setCurrentIndex(minutes_map[saved_minutes])

        top_row = QHBoxLayout()
        lr1 = QLabel("日期范围:")
        lr2 = QLabel("榜单类型:")
        lr3 = QLabel("数据清除:")
        # 自动抓取容器（复选+间隔）
        auto_wrap = QWidget(); auto_wrap_l = QHBoxLayout(auto_wrap)
        auto_wrap_l.setContentsMargins(0,0,0,0)
        auto_wrap_l.addWidget(self.auto_crawl_checkbox)
        auto_wrap_l.addWidget(self.auto_crawl_interval)
        auto_wrap_l.addStretch()
        
        top_row.addWidget(lr1); top_row.addWidget(self.date_range_combo)
        top_row.addSpacing(10)
        top_row.addWidget(lr2); top_row.addWidget(self.rank_type_combo)
        top_row.addSpacing(10)
        top_row.addWidget(lr3); top_row.addWidget(self.cleanup_combo)
        top_row.addSpacing(10)
        # 不显示“自动抓取”标题，直接显示复选+间隔
        top_row.addWidget(auto_wrap)
        top_row.addStretch()
        layout.addRow(top_row)

        # 延时：一行显示3个（类目间/子类目间/商品处理）
        delay_row = QHBoxLayout()
        delay_row.setSpacing(10)
        def get_int(key, default):
            v = app_config.get(key, default)
            if isinstance(v, dict):
                v = v.get("value", default)
            try:
                return int(v)
            except Exception:
                return default
        self.category_delay_spin = QSpinBox(); self.category_delay_spin.setRange(0,60); self.category_delay_spin.setSuffix(" 秒"); self.category_delay_spin.setValue(get_int("delay.category_delay",3))
        self.subcategory_delay_spin = QSpinBox(); self.subcategory_delay_spin.setRange(0,60); self.subcategory_delay_spin.setSuffix(" 秒"); self.subcategory_delay_spin.setValue(get_int("delay.subcategory_delay",2))
        self.goods_delay_spin = QSpinBox(); self.goods_delay_spin.setRange(0,60); self.goods_delay_spin.setSuffix(" 秒"); self.goods_delay_spin.setValue(get_int("delay.goods_delay",1))
        self.product_video_delay_spin = QSpinBox(); self.product_video_delay_spin.setRange(0,60); self.product_video_delay_spin.setSuffix(" 秒"); self.product_video_delay_spin.setValue(get_int("delay.product_video_delay",1))
        self.retry_delay_spin = QSpinBox(); self.retry_delay_spin.setRange(0,60); self.retry_delay_spin.setSuffix(" 秒"); self.retry_delay_spin.setValue(get_int("delay.retry_delay",5))
        # 字体稍小一点
        small_lbl_style = "font-size: 12px; color: #374151;"
        small_spin_style = "font-size: 12px; min-width: 70px;"
        self.category_delay_spin.setStyleSheet(small_spin_style)
        self.subcategory_delay_spin.setStyleSheet(small_spin_style)
        self.goods_delay_spin.setStyleSheet(small_spin_style)
        self.product_video_delay_spin.setStyleSheet(small_spin_style)
        self.retry_delay_spin.setStyleSheet(small_spin_style)
        l1 = QLabel("类目间延迟:"); l1.setStyleSheet(small_lbl_style)
        l2 = QLabel("子类目间延迟:"); l2.setStyleSheet(small_lbl_style)
        l3 = QLabel("商品处理延迟:"); l3.setStyleSheet(small_lbl_style)
        l4 = QLabel("视频请求延迟:"); l4.setStyleSheet(small_lbl_style)
        l5 = QLabel("重试延迟:"); l5.setStyleSheet(small_lbl_style)
        # 一行显示5项
        delay_row.addWidget(l1); delay_row.addWidget(self.category_delay_spin)
        delay_row.addSpacing(8)
        delay_row.addWidget(l2); delay_row.addWidget(self.subcategory_delay_spin)
        delay_row.addSpacing(8)
        delay_row.addWidget(l3); delay_row.addWidget(self.goods_delay_spin)
        delay_row.addSpacing(8)
        delay_row.addWidget(l4); delay_row.addWidget(self.product_video_delay_spin)
        delay_row.addSpacing(8)
        delay_row.addWidget(l5); delay_row.addWidget(self.retry_delay_spin)
        delay_row.addStretch()
        layout.addRow(delay_row)

        # 数据清除（已在顶部行定义）

        # 自动抓取（已在顶部行定义）

        # 按钮
        # 顶部控制区：开始、停止、清除日志
        ctrl_row = QHBoxLayout()
        self.start_btn = QPushButton("开始抓取")
        # 设置开始按钮样式，禁用时灰色
        self.start_btn.setStyleSheet("""
            QPushButton { background-color: #2563eb; color: white; border: none; padding: 8px 14px; border-radius: 8px; font-weight: 600; }
            QPushButton:hover { background-color: #1d4ed8; }
            QPushButton:disabled { background-color: #6c757d; color: #ffffff; }
        """)
        self.start_btn.clicked.connect(self.on_start_crawl)
        self.stop_btn = QPushButton("停止任务")
        self.stop_btn.setObjectName("secondary")
        self.stop_btn.setEnabled(False)
        self.stop_btn.clicked.connect(self.stop_task)
        clear_btn = QPushButton("清除日志")
        clear_btn.setObjectName("secondary")
        clear_btn.clicked.connect(self.clear_log)
        ctrl_row.addStretch(); ctrl_row.addWidget(clear_btn); ctrl_row.addWidget(self.stop_btn); ctrl_row.addWidget(self.start_btn)
        layout.addRow(ctrl_row)

        # 进度与统计
        progress_box = QHBoxLayout()
        self.progress_bar = QProgressBar(); self.progress_bar.setRange(0, 1); self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(False)
        self.stats_label = QLabel("等待开始…")
        self.stats_label.setStyleSheet("color:#2563eb; font-weight:600;")
        progress_box.addWidget(self.progress_bar, 3)
        progress_box.addWidget(self.stats_label, 2)
        layout.addRow(progress_box)

        # 日志控制台（暗色风格）
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setStyleSheet("""
            QTextEdit {
                background-color: #2b2b2b;
                color: #ffffff;
                font-family: 'Consolas', 'Monaco', monospace;
                font-size: 12px;
                border: 1px solid #555;
                border-radius: 6px;
                padding: 8px;
            }
        """)
        log_group = QGroupBox("")
        lg = QVBoxLayout(log_group)
        self.log_text.setMinimumHeight(320)
        lg.addWidget(self.log_text)
        layout.addRow(log_group)

    def _save_to_config(self):
        from config import config as app_config
        # 保存类目
        selected_categories = [name for name, cb in self.category_checkboxes if cb.isChecked()]
        app_config.set("crawl.selected_categories", selected_categories)
        app_config.set("crawl.date_range", self.date_range_combo.currentText())
        app_config.set("crawl.rank_type", self.rank_type_combo.currentText())
        app_config.set("crawl.cleanup_option", self.cleanup_combo.currentText())
        # 延时
        app_config.set("delay.category_delay", self.category_delay_spin.value())
        app_config.set("delay.subcategory_delay", self.subcategory_delay_spin.value())
        app_config.set("delay.goods_delay", self.goods_delay_spin.value())
        app_config.set("delay.product_video_delay", self.product_video_delay_spin.value())
        app_config.set("delay.retry_delay", self.retry_delay_spin.value())
        # 自动
        minutes_map = {"30分钟":30, "1小时":60, "2小时":120, "4小时":240}
        app_config.set("crawl.auto.enabled", self.auto_crawl_checkbox.isChecked())
        app_config.set("crawl.auto.interval_minutes", minutes_map.get(self.auto_crawl_interval.currentText(), 0))

    def get_config(self):
        self._save_to_config()
        selected_categories = [name for name, cb in self.category_checkboxes if cb.isChecked()]
        minutes_map = {"30分钟":30, "1小时":60, "2小时":120, "4小时":240}
        return {
            "selected_categories": selected_categories if selected_categories else None,
            "date_range": self.date_range_combo.currentText(),
            "rank_type": self.rank_type_combo.currentText(),
            "delay_config": {
                "delay.category_delay": self.category_delay_spin.value(),
                "delay.subcategory_delay": self.subcategory_delay_spin.value(),
                "delay.goods_delay": self.goods_delay_spin.value(),
                "delay.product_video_delay": self.product_video_delay_spin.value(),
                "delay.retry_delay": self.retry_delay_spin.value()
            },
            "cleanup_option": self.cleanup_combo.currentText(),
            "auto": {
                "enabled": self.auto_crawl_checkbox.isChecked(),
                "interval_minutes": minutes_map.get(self.auto_crawl_interval.currentText(), 0)
            }
        }

    def on_start_crawl(self):
        cfg = self.get_config()
        # 立即置灰“开始”、启用“停止”
        self.task_started()
        if hasattr(self.main_window, 'start_crawl_from_panel'):
            self.main_window.start_crawl_from_panel(cfg)

    def set_task_object(self, task_object):
        self.task_object = task_object

    def stop_task(self):
        if hasattr(self, 'task_object') and self.task_object:
            try:
                self.task_object.stop()
                if hasattr(self.main_window, 'mark_crawl_stopped'):
                    self.main_window.mark_crawl_stopped()
                self.append_log("任务已停止")
            except Exception as e:
                self.append_log(f"ERROR: 停止任务失败: {e}")
        self.stop_btn.setEnabled(False)
        # 恢复停止按钮为默认样式（灰色）
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0,1); self.progress_bar.setValue(1)

    def task_started(self):
        self.stop_btn.setEnabled(True)
        self.start_btn.setEnabled(False)
        # 停止按钮显示红色强调
        self.stop_btn.setStyleSheet("""
            QPushButton { background-color: #dc3545; color: white; }
            QPushButton:hover { background-color: #c82333; }
        """)
        # 重置进度条并开启不确定动画
        try:
            self.progress_bar.reset()
        except Exception:
            pass
        self.progress_bar.setValue(0)
        self.progress_bar.setRange(0,0)  # 不确定进度
        # 重置运行期统计
        self._goods_count = 0
        self._video_count = 0
        # 刷新今日累计基数（跨天时自动清零）
        self._load_today_base_counts()
        self._update_stats_label_text()

    def task_finished(self):
        self.stop_btn.setEnabled(False)
        # 恢复停止按钮为默认样式（灰色）
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0,1); self.progress_bar.setValue(1)

    def on_progress(self, message: str):
        # 追加日志
        self.append_log(message)
        # 解析实时统计并显示（防止数值回退）
        try:
            import re
            new_g, new_v = None, None
            # 匹配“总计: X 个商品, Y 个视频”或类似格式
            goods_match = re.search(r"总计[：:]\s*(\d+)\s*个商品", message)
            videos_match = re.search(r"(\d+)\s*个视频", message)
            if goods_match and videos_match:
                new_g = int(goods_match.group(1))
                new_v = int(videos_match.group(1))
            # 完成汇总
            if ("数据抓取完成" in message and "保存了" in message) or ("数据抓取完成!" in message):
                end_goods = re.search(r"保存了\s*(\d+)\s*个商品", message)
                end_videos = re.search(r"(\d+)\s*个视频", message)
                if end_goods and end_videos:
                    new_g = int(end_goods.group(1))
                    new_v = int(end_videos.group(1))
            # 只有当解析到的新值不小于当前值时才刷新（避免一会儿又变成0的闪屏）
            if new_g is not None and new_v is not None:
                if new_g < self._goods_count or new_v < self._video_count:
                    return
                self._goods_count, self._video_count = new_g, new_v
                self._update_stats_label_text()
        except Exception:
            pass

    def append_log(self, message: str):
        from datetime import datetime
        timestamp = datetime.now().strftime("%H:%M:%S")
        # 颜色规则复用风控重试
        if "[调试]" in message:
            color = "#888888"
        elif "ERROR" in message or "错误" in message or "失败" in message:
            color = "#ff6b6b"
        elif "成功" in message or "完成" in message or "✅" in message:
            color = "#00ff00"
        elif "警告" in message or "⚠" in message:
            color = "#ffaa00"
        elif "开始" in message or "🔄" in message:
            color = "#00bfff"
        else:
            color = "#ffffff"
        log_entry = f'<span style="color: {color};">[{timestamp}] {message}</span>'
        self.log_text.append(log_entry)
        cursor = self.log_text.textCursor()
        cursor.movePosition(cursor.End)
        self.log_text.setTextCursor(cursor)

    def clear_log(self):
        self.log_text.clear()


# 面板：素材榜数据抓取配置（按照网页筛选维度设计）
class MaterialCrawlPanel(QWidget):
    """素材榜数据抓取面板

    维度参考网页：所属行业、推广类型、视频长度、三日榜/周榜/月榜、排序指标等。
    """

    def __init__(self, main_window):
        super().__init__(main_window)
        self.main_window = main_window
        self.task_object = None
        self.setup_ui()

    def setup_ui(self):
        # 样式尽量复用商品榜面板风格
        self.setStyleSheet("""
            QWidget { font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif; }
            QGroupBox { font-weight: bold; border: 1px solid #e5e7eb; border-radius: 10px; margin-top: 8px; padding-top: 10px; background: #ffffff; }
            QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 6px; color: #374151; }
            QLabel { color: #374151; font-size: 13px; }
            QCheckBox { spacing: 4px; padding: 4px 6px; border-radius: 6px; }
            QCheckBox::indicator { width: 16px; height: 16px; border: 1px solid #94a3b8; border-radius: 3px; background: #fff; }
            QComboBox, QSpinBox, QLineEdit {
                padding: 6px 10px; border: 1px solid #e5e7eb; border-radius: 8px; 
                background-color: #ffffff; color: #1f2937;
                min-width: 110px; min-height: 28px; font-size: 13px;
            }
            QComboBox:hover { border: 1px solid #3b82f6; background-color: #f0f9ff; }
            QComboBox:focus, QSpinBox:focus, QLineEdit:focus { border: 1px solid #3b82f6; background-color: #ffffff; }
            QComboBox:disabled { background-color: #f3f4f6; color: #9ca3af; border: 1px solid #e5e7eb; }
            QComboBox::drop-down {
                subcontrol-origin: padding; subcontrol-position: top right; width: 30px;
                border-left: 1px solid #e5e7eb; border-top-right-radius: 8px; border-bottom-right-radius: 8px;
                background-color: #f9fafb;
            }
            QComboBox::drop-down:hover { background-color: #e0f2fe; }
            QComboBox::drop-down:disabled { background-color: #f3f4f6; }
            QComboBox::down-arrow {
                image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #6b7280;
                width: 0px; height: 0px; margin-right: 8px;
            }
            QComboBox::down-arrow:hover { border-top-color: #2563eb; }
            QComboBox::down-arrow:disabled { border-top-color: #d1d5db; }
            QComboBox QAbstractItemView {
                background-color: #ffffff; color: #111827;
                selection-background-color: #3b82f6; selection-color: #ffffff;
                border: 1px solid #d1d5db; border-radius: 6px; padding: 4px; outline: none;
            }
            QComboBox QAbstractItemView::item { min-height: 28px; padding: 4px 10px; border-radius: 4px; }
            QComboBox QAbstractItemView::item:hover { background-color: #dbeafe; color: #1e40af; }
            QPushButton { padding: 8px 14px; border: none; border-radius: 8px; background-color: #2563eb; color: white; font-weight: 600; }
            QPushButton:hover { background-color: #1d4ed8; }
            QPushButton#secondary { background-color: #f3f4f6; color: #111827; }
            QPushButton#secondary:hover { background-color: #e5e7eb; }
        """)

        outer = QVBoxLayout(self)
        outer.setSpacing(8)
        outer.setContentsMargins(4, 4, 4, 4)

        # 顶部：筛选条件（参考网页）
        filters_group = QGroupBox("")
        fg_layout = QGridLayout(filters_group)
        fg_layout.setHorizontalSpacing(12)
        fg_layout.setVerticalSpacing(8)

        # 所属行业
        self.industry_combo = QComboBox()
        self.industry_combo.addItem("全部", "")
        self.industry_combo.addItem("彩妆/香水/美妆工具", "彩妆/香水/美妆工具")
        self.industry_combo.addItem("身体护理", "身体护理")
        self.industry_combo.addItem("农资", "农资")
        self.industry_combo.addItem("冲调/水饮", "冲调/水饮")
        self.industry_combo.addItem("3C配件", "3C配件")
        self.industry_combo.addItem("居家百货", "居家百货")
        self.industry_combo.addItem("男装", "男装")
        self.industry_combo.addItem("女装", "女装")
        self.industry_combo.addItem("其他", "其他")
        
        # 自动遍历所有行业复选框
        self.auto_traverse_checkbox = QCheckBox("自动遍历所有行业")
        self.auto_traverse_checkbox.setToolTip("勾选后将自动抓取所有行业的数据，忽略上面的行业选择")
        self.auto_traverse_checkbox.setStyleSheet("""
            QCheckBox { spacing: 6px; padding: 6px 8px; border-radius: 6px; background-color: #f0f9ff; }
            QCheckBox::indicator:checked { background-color: #2563eb; border-color: #2563eb; }
            QCheckBox:hover { background-color: #dbeafe; }
        """)
        # 连接变化信号，勾选时禁用行业下拉框
        self.auto_traverse_checkbox.stateChanged.connect(self._on_auto_traverse_changed)

        # 推广类型
        self.promo_type_combo = QComboBox()
        self.promo_type_combo.addItem("不限", "")
        self.promo_type_combo.addItem("商品推广", "GOODS")
        self.promo_type_combo.addItem("直播推广", "LIVE")

        # 视频长度
        self.duration_combo = QComboBox()
        self.duration_combo.addItem("不限", "")
        self.duration_combo.addItem("0-20秒", "0_20")
        self.duration_combo.addItem("20-40秒", "20_40")
        self.duration_combo.addItem("40-60秒", "40_60")
        self.duration_combo.addItem("60秒以上", "60_PLUS")

        # 榜单时间（三日榜 / 周榜 / 月榜）
        self.time_range_combo = QComboBox()
        self.time_range_combo.addItems(["三日榜", "周榜", "月榜"])
        # 作为参数值的编码（实际接口可根据抓包再调整）
        self.time_range_codes = {"三日榜": "3d", "周榜": "7d", "月榜": "30d"}

        # 排序指标（消耗、曝光、体验分、点击率、转化率、点赞率、关注率、评论数）
        self.metric_combo = QComboBox()
        self.metric_combo.addItem("消耗", "consume")
        self.metric_combo.addItem("曝光", "exposure")
        self.metric_combo.addItem("体验分", "score")
        self.metric_combo.addItem("点击率", "ctr")
        self.metric_combo.addItem("转化率", "cvr")
        self.metric_combo.addItem("点赞率", "like_rate")
        self.metric_combo.addItem("关注率", "follow_rate")
        self.metric_combo.addItem("评论数", "comment")

        # 商品类目 / 关键词（右侧搜索框）
        self.keyword_edit = QLineEdit()
        self.keyword_edit.setPlaceholderText("请输入商品类目或关键字，可留空")

        # 摆放到网格：
        fg_layout.addWidget(QLabel("所属行业:"), 0, 0)
        fg_layout.addWidget(self.industry_combo, 0, 1)
        fg_layout.addWidget(self.auto_traverse_checkbox, 0, 2, 1, 2)  # 跨两列
        fg_layout.addWidget(QLabel("视频长度:"), 0, 4)
        fg_layout.addWidget(self.duration_combo, 0, 5)

        fg_layout.addWidget(QLabel("推广类型:"), 1, 0)
        fg_layout.addWidget(self.promo_type_combo, 1, 1)
        fg_layout.addWidget(QLabel("榜单时间:"), 1, 2)
        fg_layout.addWidget(self.time_range_combo, 1, 3)
        fg_layout.addWidget(QLabel("排序指标:"), 1, 4)
        fg_layout.addWidget(self.metric_combo, 1, 5)
        
        fg_layout.addWidget(QLabel("商品类目/关键词:"), 2, 0)
        fg_layout.addWidget(self.keyword_edit, 2, 1, 1, 5)  # 跨五列

        outer.addWidget(filters_group)

        # 按钮行：开始 / 停止 / 清除日志
        ctrl_row = QHBoxLayout()
        self.start_btn = QPushButton("开始抓取")
        self.start_btn.setStyleSheet("""
            QPushButton { background-color: #2563eb; color: white; border: none; padding: 8px 14px; border-radius: 8px; font-weight: 600; }
            QPushButton:hover { background-color: #1d4ed8; }
            QPushButton:disabled { background-color: #6c757d; color: #ffffff; }
        """)
        self.start_btn.clicked.connect(self.on_start_crawl)
        self.stop_btn = QPushButton("停止任务")
        self.stop_btn.setObjectName("secondary")
        self.stop_btn.setEnabled(False)
        self.stop_btn.clicked.connect(self.stop_task)
        clear_btn = QPushButton("清除日志")
        clear_btn.setObjectName("secondary")
        clear_btn.clicked.connect(self.clear_log)
        ctrl_row.addStretch(); ctrl_row.addWidget(clear_btn); ctrl_row.addWidget(self.stop_btn); ctrl_row.addWidget(self.start_btn)
        outer.addLayout(ctrl_row)

        # 进度与提示
        bar_row = QHBoxLayout()
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(False)
        self.stats_label = QLabel("等待开始…")
        self.stats_label.setStyleSheet("color:#2563eb; font-weight:600;")
        bar_row.addWidget(self.progress_bar, 3)
        bar_row.addWidget(self.stats_label, 2)
        outer.addLayout(bar_row)

        # 日志控制台
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setStyleSheet("""
            QTextEdit {
                background-color: #2b2b2b;
                color: #ffffff;
                font-family: 'Consolas', 'Monaco', monospace;
                font-size: 12px;
                border: 1px solid #555;
                border-radius: 6px;
                padding: 8px;
            }
        """)
        self.log_text.setMinimumHeight(320)
        outer.addWidget(self.log_text)

    def _on_auto_traverse_changed(self):
        """自动遍历复选框状态变化时的处理"""
        is_auto = self.auto_traverse_checkbox.isChecked()
        # 勾选时禁用行业下拉框，取消勾选时启用
        self.industry_combo.setEnabled(not is_auto)
        if is_auto:
            self.append_log("✅ 已启用自动遍历所有行业模式")

    def get_config(self):
        """构造传给 MaterialCrawler 的筛选参数"""
        # 将当前选择转成简单的 filters 字典，键名先按直观命名，后续可在爬虫内部映射到真实接口参数
        time_text = self.time_range_combo.currentText()
        time_code = self.time_range_codes.get(time_text, "")
        
        # 检查是否启用自动遍历
        auto_traverse = self.auto_traverse_checkbox.isChecked()
        
        filters = {
            "industry": self.industry_combo.currentData() if not auto_traverse else "",
            "promotionType": self.promo_type_combo.currentData(),
            "videoDuration": self.duration_combo.currentData(),
            "timeRange": time_code,
            "sortMetric": self.metric_combo.currentData(),
            "auto_traverse_industries": auto_traverse,  # 标记是否自动遍历
        }
        kw = self.keyword_edit.text().strip()
        if kw:
            filters["keyword"] = kw
        return {"filters": filters}

    def on_start_crawl(self):
        cfg = self.get_config()
        # 立即置灰“开始”、启用“停止”
        self.task_started()
        if hasattr(self.main_window, 'start_material_crawl_from_panel'):
            self.main_window.start_material_crawl_from_panel(cfg)

    def set_task_object(self, task_object):
        self.task_object = task_object

    def task_started(self):
        self.stop_btn.setEnabled(True)
        self.start_btn.setEnabled(False)
        # 停止按钮显示红色强调
        self.stop_btn.setStyleSheet("""
            QPushButton { background-color: #dc3545; color: white; }
            QPushButton:hover { background-color: #c82333; }
        """)
        # 进度条重置为不确定状态
        try:
            self.progress_bar.reset()
        except Exception:
            pass
        self.progress_bar.setValue(0)
        self.progress_bar.setRange(0, 0)
        self.stats_label.setText("正在抓取素材榜数据…")

    def task_finished(self):
        self.stop_btn.setEnabled(False)
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(1)
        self.stats_label.setText("任务已完成")

    def on_progress(self, message: str):
        # 显示日志 + 用最后一条进度覆盖状态文字
        self.append_log(message)
        try:
            self.stats_label.setText(message)
        except Exception:
            pass

    def append_log(self, message: str):
        from datetime import datetime
        timestamp = datetime.now().strftime("%H:%M:%S")
        if "ERROR" in message or "错误" in message or "失败" in message:
            color = "#ff6b6b"
        elif "成功" in message or "完成" in message or "✅" in message:
            color = "#00ff00"
        elif "警告" in message or "⚠" in message:
            color = "#ffaa00"
        elif "开始" in message or "🔄" in message:
            color = "#00bfff"
        else:
            color = "#ffffff"
        log_entry = f'<span style="color: {color};">[{timestamp}] {message}</span>'
        self.log_text.append(log_entry)
        cursor = self.log_text.textCursor()
        cursor.movePosition(cursor.End)
        self.log_text.setTextCursor(cursor)

    def clear_log(self):
        self.log_text.clear()

    def stop_task(self):
        """停止素材榜抓取任务"""
        if hasattr(self, 'task_object') and self.task_object:
            try:
                self.task_object.stop()
                self.append_log("任务已停止")
            except Exception as e:
                self.append_log(f"ERROR: 停止任务失败: {e}")
        self.stop_btn.setEnabled(False)
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(1)
        self.stats_label.setText("任务已停止")


# 面板：视频下载配置（用于Tab，不弹窗）
class DownloadConfigPanel(QWidget):
    def __init__(self, main_window):
        super().__init__(main_window)
        self.main_window = main_window
        self.download_path = ""
        # 今日累计基数（当天已累计的下载视频数，不含“本次”正在下载的数量）
        self._today_base_downloaded = 0
        self._load_today_base_downloaded()
        self.setup_ui()

    def setup_ui(self):
        # 美化样式
        self.setStyleSheet("""
            QWidget { font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif; }
            QGroupBox { font-weight: bold; border: 1px solid #e5e7eb; border-radius: 10px; margin-top: 8px; padding-top: 10px; background: #ffffff; }
            QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 6px; color: #374151; }
            QLabel { color: #374151; font-size: 13px; }
            QCheckBox { spacing: 4px; padding: 4px 6px; border-radius: 6px; }
            QCheckBox::indicator { width: 16px; height: 16px; border: 1px solid #94a3b8; border-radius: 3px; background: #fff; }
            QCheckBox::indicator:checked { background-color: #059669; border-color: #059669; }
            QCheckBox:checked { background-color: #ecfdf5; color: #065f46; border: 1px solid #34d399; }
            QCheckBox:hover { background-color: #f3f4f6; }
            QComboBox, QSpinBox, QLineEdit {
                padding: 6px 10px; border: 1px solid #e5e7eb; border-radius: 8px; background-color: white;
                min-width: 110px; min-height: 28px; font-size: 13px;
            }
            QAbstractSpinBox { padding-right: 26px; }
            QSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 18px; height: 13px; }
            QSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 18px; height: 13px; }
            QComboBox:focus, QSpinBox:focus, QLineEdit:focus { border: 1px solid #3b82f6; }
            QPushButton { padding: 8px 14px; border: none; border-radius: 8px; background-color: #059669; color: white; font-weight: 600; }
            QPushButton:hover { background-color: #047857; }
            QPushButton#secondary { background-color: #f3f4f6; color: #111827; }
            QPushButton#secondary:hover { background-color: #e5e7eb; }
        """)
        
        outer = QVBoxLayout(self)
        outer.setSpacing(8)
        outer.setContentsMargins(4, 4, 4, 4)
        # 顶部标题移除
        layout = QFormLayout()
        layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
        layout.setHorizontalSpacing(14)
        layout.setVerticalSpacing(8)
        outer.addLayout(layout)

        # 路径
        path_layout = QHBoxLayout()
        from config import config as app_config
        saved_path = app_config.get("download.default_path", "ks-download")
        if isinstance(saved_path, str) and saved_path != "ks-download":
            self.download_path = saved_path
        else:
            default_download_path = os.path.join(os.getcwd(), "ks-download")
            self.download_path = default_download_path
        self.path_edit = QLineEdit(self.download_path)
        self.path_edit.setReadOnly(True)
        path_btn = QPushButton("选择文件夹")
        path_btn.clicked.connect(self.select_path)
        path_layout.addWidget(self.path_edit)
        path_layout.addWidget(path_btn)

        # 参数（栅格紧凑布局：同一行多个）
        params_group = QGroupBox("")
        pgrid = QGridLayout(); pgrid.setHorizontalSpacing(10); pgrid.setVerticalSpacing(6)
        # 线程数
        self.thread_spin = QSpinBox(); self.thread_spin.setRange(1,10)
        saved_threads = app_config.get("download.max_threads", 3)
        self.thread_spin.setValue(int(saved_threads) if isinstance(saved_threads, int) else 3)
        self.thread_spin.setSuffix(" 个线程")
        # 视频请求延迟
        self.video_request_delay_spin = QSpinBox(); self.video_request_delay_spin.setRange(0,60); self.video_request_delay_spin.setSuffix(" 秒")
        saved_video_delay = app_config.get("download.video_request_delay", 15)
        self.video_request_delay_spin.setValue(int(saved_video_delay) if isinstance(saved_video_delay, int) else 15)
        # 不过滤佣金
        self.no_commission_filter_checkbox = QCheckBox("不过滤佣金(全部下载)")
        saved_no_filter = app_config.get("download.no_commission_filter", False)
        if isinstance(saved_no_filter, dict):
            saved_no_filter = saved_no_filter.get("value", False)
        self.no_commission_filter_checkbox.setChecked(bool(saved_no_filter))

        # 子类目
        self.enable_subcategory_checkbox = QCheckBox("开启子类目(按 类目/子类目/佣金段 分类)")
        saved_enable_sub = app_config.get("download.enable_subcategory", False)
        if isinstance(saved_enable_sub, dict):
            saved_enable_sub = saved_enable_sub.get("value", False)
        self.enable_subcategory_checkbox.setChecked(bool(saved_enable_sub))
        # 自动下载
        self.auto_download_checkbox = QCheckBox("开启自动下载")
        saved_auto = app_config.get("download.auto.enabled", False)
        if isinstance(saved_auto, dict):
            saved_auto = saved_auto.get("value", False)
        self.auto_download_checkbox.setChecked(bool(saved_auto))
        self.auto_interval_combo = QComboBox(); self.auto_interval_combo.addItems(["10分钟", "30分钟", "1小时", "2小时"])
        saved_minutes = app_config.get("download.auto.interval_minutes", 0)
        try:
            saved_minutes = int(saved_minutes if not isinstance(saved_minutes, dict) else saved_minutes.get("value", 0))
        except Exception:
            saved_minutes = 0
        minutes_to_index = {10:0, 30:1, 60:2, 120:3}
        if saved_minutes in minutes_to_index:
            self.auto_interval_combo.setCurrentIndex(minutes_to_index[saved_minutes])
        # 顶部一行：下载路径 | 开启子类目 | 开启自动下载(含间隔)
        top_row = QHBoxLayout()
        lpath = QLabel("下载路径:")
        top_row.addWidget(lpath)
        top_row.addLayout(path_layout)
        top_row.addSpacing(10)
        top_row.addWidget(self.enable_subcategory_checkbox)
        top_row.addSpacing(10)
        top_row.addWidget(self.no_commission_filter_checkbox)
        top_row.addSpacing(10)
        auto_wrap = QWidget(); auto_wrap_l = QHBoxLayout(auto_wrap)
        auto_wrap_l.setContentsMargins(0,0,0,0)
        auto_wrap_l.addWidget(self.auto_download_checkbox)
        auto_wrap_l.addWidget(self.auto_interval_combo)
        auto_wrap_l.addStretch()
        top_row.addWidget(auto_wrap)
        top_row.addStretch()
        layout.addRow(top_row)

        # 参数行：线程数 | 视频请求延迟
        pgrid.addWidget(QLabel("线程数:"), 0, 0); pgrid.addWidget(self.thread_spin, 0, 1)
        pgrid.addWidget(QLabel("视频请求延迟:"), 0, 2); pgrid.addWidget(self.video_request_delay_spin, 0, 3)
        params_group.setLayout(pgrid)
        layout.addRow(params_group)

        # 按钮
        # 顶部控制区：开始、停止、清除日志
        ctrl_row = QHBoxLayout()
        self.start_btn = QPushButton("开始下载")
        # 设置开始按钮样式，禁用时灰色
        self.start_btn.setStyleSheet("""
            QPushButton { background-color: #059669; color: white; border: none; padding: 8px 14px; border-radius: 8px; font-weight: 600; }
            QPushButton:hover { background-color: #047857; }
            QPushButton:disabled { background-color: #6c757d; color: #ffffff; }
        """)
        self.start_btn.clicked.connect(self.on_start_download)
        self.stop_btn = QPushButton("停止任务")
        self.stop_btn.setObjectName("secondary")
        self.stop_btn.setEnabled(False)
        self.stop_btn.clicked.connect(self.stop_task)
        clear_btn = QPushButton("清除日志")
        clear_btn.setObjectName("secondary")
        clear_btn.clicked.connect(self.clear_log)
        ctrl_row.addStretch(); ctrl_row.addWidget(clear_btn); ctrl_row.addWidget(self.stop_btn); ctrl_row.addWidget(self.start_btn)
        layout.addRow(ctrl_row)

        # 进度与统计
        progress_box = QHBoxLayout()
        self.progress_bar = QProgressBar(); self.progress_bar.setRange(0, 1); self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(False)
        self.stats_label = QLabel("等待开始…")
        self.stats_label.setStyleSheet("color:#059669; font-weight:600;")
        progress_box.addWidget(self.progress_bar, 3)
        progress_box.addWidget(self.stats_label, 2)
        layout.addRow(progress_box)

        # 日志控制台（暗色风格）
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setStyleSheet("""
            QTextEdit {
                background-color: #2b2b2b;
                color: #ffffff;
                font-family: 'Consolas', 'Monaco', monospace;
                font-size: 12px;
                border: 1px solid #555;
                border-radius: 6px;
                padding: 8px;
            }
        """)
        log_group = QGroupBox("")
        lg = QVBoxLayout(log_group)
        self.log_text.setMinimumHeight(320)
        lg.addWidget(self.log_text)
        layout.addRow(log_group)

    def select_path(self):
        path = QFileDialog.getExistingDirectory(self, "选择下载文件夹")
        if path:
            self.path_edit.setText(path)
            self.download_path = path

    def _save_to_config(self):
        from config import config as app_config
        app_config.set("download.default_path", self.download_path)
        app_config.set("download.max_threads", self.thread_spin.value())
        app_config.set("download.video_request_delay", self.video_request_delay_spin.value())
        app_config.set("download.enable_subcategory", self.enable_subcategory_checkbox.isChecked())
        app_config.set("download.no_commission_filter", self.no_commission_filter_checkbox.isChecked())
        minutes_map = {"10分钟":10, "30分钟":30, "1小时":60, "2小时":120}
        app_config.set("download.auto.enabled", self.auto_download_checkbox.isChecked())
        app_config.set("download.auto.interval_minutes", minutes_map.get(self.auto_interval_combo.currentText(), 0))

    def get_config(self):
        self._save_to_config()
        minutes_map = {"10分钟":10, "30分钟":30, "1小时":60, "2小时":120}
        return {
            "download_path": self.download_path,
            "thread_count": self.thread_spin.value(),
            "video_request_delay": self.video_request_delay_spin.value(),
            "enable_subcategory": self.enable_subcategory_checkbox.isChecked(),
            "no_commission_filter": self.no_commission_filter_checkbox.isChecked(),
            "auto": {
                "enabled": self.auto_download_checkbox.isChecked(),
                "interval_minutes": minutes_map.get(self.auto_interval_combo.currentText(), 0)
            }
        }

    def on_start_download(self):
        cfg = self.get_config()
        # 立即置灰“开始”、启用“停止”
        self.task_started()
        if hasattr(self.main_window, 'start_download_from_panel'):
            self.main_window.start_download_from_panel(cfg)

    def set_task_object(self, task_object):
        self.task_object = task_object

    def stop_task(self):
        if hasattr(self, 'task_object') and self.task_object:
            try:
                self.task_object.stop()
                if hasattr(self.main_window, 'mark_download_stopped'):
                    self.main_window.mark_download_stopped()
                self.append_log("任务已停止")
            except Exception as e:
                self.append_log(f"ERROR: 停止任务失败: {e}")
        self.stop_btn.setEnabled(False)
        # 恢复停止按钮为默认样式（灰色）
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0,1); self.progress_bar.setValue(1)

    def task_started(self):
        self.stop_btn.setEnabled(True)
        self.start_btn.setEnabled(False)
        # 停止按钮显示红色强调
        self.stop_btn.setStyleSheet("""
            QPushButton { background-color: #dc3545; color: white; }
            QPushButton:hover { background-color: #c82333; }
        """)
        # 重置进度条并开启不确定动画
        try:
            self.progress_bar.reset()
        except Exception:
            pass
        self.progress_bar.setValue(0)
        self.progress_bar.setRange(0,0)
        # 刷新今日累计基数（跨天时自动清零）
        self._load_today_base_downloaded()
        # 初始显示包含今日累计
        try:
            self.stats_label.setText(f"正在下载… ｜ 今日累计：{self._today_base_downloaded} 个视频")
        except Exception:
            self.stats_label.setText("正在下载…")

    def task_finished(self):
        self.stop_btn.setEnabled(False)
        # 恢复停止按钮为默认样式（灰色）
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0,1); self.progress_bar.setValue(1)

    def on_progress(self, message: str):
        self.append_log(message)
        try:
            import re
            dl_match = re.search(r"累计下载[：:]?\s*(\d+)\s*个视频", message)
            if dl_match:
                n = int(dl_match.group(1))
                total_today = self._today_base_downloaded + n
                self.stats_label.setText(f"已下载视频数：{n}，并且已分好类目 ｜ 今日累计：{total_today} 个视频")
        except Exception:
            pass

    def append_log(self, message: str):
        from datetime import datetime
        timestamp = datetime.now().strftime("%H:%M:%S")
        if "[调试]" in message:
            color = "#888888"
        elif "ERROR" in message or "错误" in message or "失败" in message:
            color = "#ff6b6b"
        elif "成功" in message or "完成" in message or "✅" in message:
            color = "#00ff00"
        elif "警告" in message or "⚠" in message:
            color = "#ffaa00"
        elif "开始" in message or "🔄" in message or "下载" in message:
            color = "#00bfff"
        else:
            color = "#ffffff"
        log_entry = f'<span style="color: {color};">[{timestamp}] {message}</span>'
        self.log_text.append(log_entry)
        cursor = self.log_text.textCursor()
        cursor.movePosition(cursor.End)
        self.log_text.setTextCursor(cursor)

    def clear_log(self):
        self.log_text.clear()

    def _load_today_base_downloaded(self):
        """加载并校正当日累计（下载）基数"""
        try:
            from datetime import datetime
            from config import config as app_config
            today = datetime.now().strftime('%Y%m%d')
            d_date = app_config.get("stats.download.date", "")
            d_videos = app_config.get("stats.download.videos", 0)
            
            # 兼容旧版本：如果date字段不存在或为空，自动添加并清零
            if not d_date or d_date != today:
                d_videos = 0
                app_config.set("stats.download.date", today)
                app_config.set("stats.download.videos", 0)
            
            try:
                d_videos = int(d_videos)
            except Exception:
                d_videos = 0
            self._today_base_downloaded = max(0, d_videos)
        except Exception:
            self._today_base_downloaded = 0

# 进度对话框
class ProgressDialog(QDialog):
    def __init__(self, title, parent=None):
        super().__init__(parent)
        self.base_title = title
        self.setWindowTitle(title)
        self.setModal(False)  # 改为非模态对话框
        # 调大对话框尺寸
        self.resize(800, 600)
        self.task_object = None  # 用于存储任务对象
        # 添加统计信息
        self.total_goods = 0
        self.total_videos = 0
        self.total_downloaded = 0
        self.setup_ui()
        
    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        # 创建日志显示区域
        self.log_text_edit = QTextEdit()
        self.log_text_edit.setReadOnly(True)
        self.log_text_edit.setStyleSheet("""
            QTextEdit {
                background-color: #f8f9fa;
                font-family: 'Courier New', monospace;
                font-size: 12px;
            }
        """)
        layout.addWidget(self.log_text_edit)
        
        # 创建按钮布局
        button_layout = QHBoxLayout()
        
        # 停止任务按钮
        self.stop_btn = QPushButton("停止任务")
        self.stop_btn.clicked.connect(self.stop_task)
        button_layout.addWidget(self.stop_btn)
        
        # 清除日志按钮
        self.clear_btn = QPushButton("清除日志")
        self.clear_btn.clicked.connect(self.clear_log)
        button_layout.addWidget(self.clear_btn)
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 0)  # Indeterminate
        button_layout.addWidget(self.progress_bar)
        
        # 关闭按钮
        self.close_btn = QPushButton("关闭")
        def close_dialog():
            self.close()
        self.close_btn.clicked.connect(close_dialog)
        self.close_btn.setEnabled(False)
        button_layout.addWidget(self.close_btn)
        
        layout.addLayout(button_layout)
        
    def set_task_object(self, task_object):
        """设置任务对象"""
        self.task_object = task_object
        
    def stop_task(self):
        """停止任务"""
        if self.task_object:
            self.task_object.stop()
            self.update_progress("任务已停止")
            self.close_btn.setEnabled(True)
            self.stop_btn.setEnabled(False)
        
    def update_progress(self, message):
        """更新进度信息"""
        # 添加调试信息，显示正在处理的消息
        if "总计" in message and ("商品" in message or "视频" in message):
            print(f"[调试] 检测到包含统计信息的消息: {message}")
            
        # 检查是否包含统计信息并更新标题栏
        if ("总计:" in message and "个商品" in message and "个视频" in message) or ("总计" in message and "商品" in message and "视频" in message):
            print(f"[调试] 匹配到实时统计信息: {message}")
            # 从消息中提取统计信息 (数据抓取任务)
            import re
            # 修改正则表达式以匹配data_crawler.py中的消息格式
            # 格式: (总计: X 个商品, Y 个视频)
            goods_match = re.search(r"总计[：:] (\d+) 个商品", message)
            if not goods_match:
                goods_match = re.search(r"总计[：:]\s*(\d+)\s*个商品", message)
            if not goods_match:
                goods_match = re.search(r"总计\s*(\d+)\s*个商品", message)
                
            videos_match = re.search(r"(\d+)\s*个视频", message)
            if goods_match and videos_match:
                g = int(goods_match.group(1))
                v = int(videos_match.group(1))
                # 防止统计回退
                if g >= self.total_goods and v >= self.total_videos:
                    self.total_goods = g
                    self.total_videos = v
                    print(f"[调试] 提取到统计数据: 商品={self.total_goods}, 视频={self.total_videos}")
                    self.update_title()
            else:
                print(f"[调试] 正则匹配失败 - goods_match: {goods_match}, videos_match: {videos_match}")
        elif "累计下载:" in message and "个视频" in message:
            # 从消息中提取统计信息 (视频下载任务)
            import re
            downloaded_match = re.search(r"累计下载:\s*(\d+)\s*个视频", message)
            if downloaded_match:
                d = int(downloaded_match.group(1))
                if d >= self.total_downloaded:
                    self.total_downloaded = d
                    self.update_title()
        elif ("数据抓取完成!" in message and "总共保存了" in message) or ("数据抓取完成" in message and "保存了" in message):
            # 最终统计信息 (数据抓取任务完成)
            import re
            # 修改正则表达式以匹配data_crawler.py中的最终消息格式
            # 格式: "数据抓取完成! 总共保存了 X 个商品，Y 个视频"
            goods_match = re.search(r"总共保存了\s*(\d+)\s*个商品", message)
            if not goods_match:
                goods_match = re.search(r"保存了\s*(\d+)\s*个商品", message)
            videos_match = re.search(r"(\d+)\s*个视频", message)
            if goods_match and videos_match:
                g = int(goods_match.group(1))
                v = int(videos_match.group(1))
                # 防止统计回退
                if g >= self.total_goods and v >= self.total_videos:
                    self.total_goods = g
                    self.total_videos = v
                    self.update_title()
        elif "视频下载任务完成!" in message and "累计下载了" in message:
            # 最终统计信息 (视频下载任务完成)
            import re
            downloaded_match = re.search(r"累计下载了\s*(\d+)\s*个视频", message)
            if downloaded_match:
                self.total_downloaded = int(downloaded_match.group(1))
                self.update_title()
        
        # 添加时间戳
        from datetime import datetime
        timestamp = datetime.now().strftime("%H:%M:%S")
        
        # 根据消息内容设置不同的颜色
        color = "#343a40"  # 默认黑色
        if "错误" in message or "失败" in message:
            color = "#dc3545"  # 红色
        elif "完成" in message or "成功" in message:
            color = "#28a745"  # 绿色
        elif "开始" in message:
            color = "#007bff"  # 蓝色
        elif "处理" in message:
            color = "#ffc107"  # 黄色
        elif "停止" in message:
            color = "#6c757d"  # 灰色
        elif ("累计下载" in message or "已下载" in message) and "下载视频" not in message:
            color = "#007bff"  # 蓝色
        elif "数据抓取" in message and ("总计" in message or "商品" in message or "视频" in message):
            color = "#ff6b35"  # 显眼的橙色
            
        # 添加带颜色的日志信息
        log_entry = f'<span style="color: {color};">[{timestamp}] {message}</span>'
        self.log_text_edit.append(log_entry)
        
        # 滚动到最新日志
        self.log_text_edit.moveCursor(QTextCursor.End)
        
    def update_title(self):
        """更新窗口标题以显示统计信息"""
        if "数据抓取" in self.base_title:
            # 修改标题格式为"数据抓取进度，已保存商品总数：44，视频 url 数：333"
            new_title = f"数据抓取进度，已保存商品总数：{self.total_goods}，视频 url 数：{self.total_videos}"
            self.setWindowTitle(new_title)
            # 立即刷新UI，提升实时性
            try:
                from PyQt5.QtWidgets import QApplication
                QApplication.processEvents()
            except Exception:
                pass
            # 添加调试信息
            print(f"[调试] 更新数据抓取标题: {new_title}")
        elif "视频下载" in self.base_title:
            # 修改标题格式为"视频下载进度：已下载视频数：{数量}，并且已分好类目"
            new_title = f"视频下载进度：已下载视频数：{self.total_downloaded}，并且已分好类目"
            self.setWindowTitle(new_title)
            # 立即刷新UI，提升实时性
            try:
                from PyQt5.QtWidgets import QApplication
                QApplication.processEvents()
            except Exception:
                pass
            # 添加调试信息
            print(f"[调试] 更新视频下载标题: {new_title}")
        
    def clear_log(self):
        """清除日志"""
        self.log_text_edit.clear()
        
    def task_finished(self):
        """任务完成"""
        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(1)
        self.close_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        self.update_progress("任务已完成")


# 面板：素材榜视频下载配置（专用版本，支持按行业/体验分等筛选）
class MaterialDownloadPanel(QWidget):
    def __init__(self, main_window):
        super().__init__(main_window)
        self.main_window = main_window
        self.task_object = None
        self.download_path = ""
        self._today_base_downloaded = 0
        self.setup_ui()
        
    def setup_ui(self):
        # 样式复用商品下载面板风格
        self.setStyleSheet("""
            QWidget { font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif; }
            QGroupBox { font-weight: bold; border: 1px solid #e5e7eb; border-radius: 10px; margin-top: 8px; padding-top: 10px; background: #ffffff; }
            QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 6px; color: #374151; }
            QLabel { color: #374151; font-size: 13px; }
            QCheckBox { spacing: 4px; padding: 4px 6px; border-radius: 6px; }
            QComboBox, QSpinBox, QLineEdit, QPushButton#browse {
                padding: 6px 10px; border: 1px solid #e5e7eb; border-radius: 8px; 
                background-color: #ffffff; color: #1f2937;
                min-width: 110px; min-height: 28px; font-size: 13px;
            }
            QComboBox:hover { border: 1px solid #3b82f6; background-color: #f0f9ff; }
            QComboBox:focus, QSpinBox:focus, QLineEdit:focus { border: 1px solid #3b82f6; background-color: #ffffff; }
            QComboBox::drop-down {
                subcontrol-origin: padding; subcontrol-position: top right; width: 24px;
                border-left: 1px solid #e5e7eb; border-top-right-radius: 8px; border-bottom-right-radius: 8px;
                background-color: #f9fafb;
            }
            QComboBox::drop-down:hover { background-color: #e0f2fe; }
            QComboBox::down-arrow {
                image: none; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #6b7280;
                width: 0px; height: 0px; margin-right: 6px;
            }
            QComboBox::down-arrow:hover { border-top-color: #2563eb; }
            QComboBox QAbstractItemView {
                background-color: #ffffff; color: #111827;
                selection-background-color: #2563eb; selection-color: #ffffff;
                border: 1px solid #d1d5db; border-radius: 6px; padding: 4px; outline: none;
            }
            QComboBox QAbstractItemView::item { min-height: 28px; padding: 4px 10px; border-radius: 4px; }
            QComboBox QAbstractItemView::item:hover { background-color: #dbeafe; color: #1e40af; }
            QPushButton { padding: 8px 14px; border: none; border-radius: 8px; background-color: #2563eb; color: white; font-weight: 600; }
            QPushButton:hover { background-color: #1d4ed8; }
            QPushButton#secondary { background-color: #f3f4f6; color: #111827; }
            QPushButton#secondary:hover { background-color: #e5e7eb; }
        """)
        
        layout = QFormLayout(self)
        layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
        layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
        layout.setSpacing(10)
        layout.setContentsMargins(12, 12, 12, 12)
        
        # 一行显示：下载路径 + 并发数 + 文件夹分类
        from config import config as app_config
        top_row = QHBoxLayout()
        
        # 下载路径
        top_row.addWidget(QLabel("下载路径:"))
        self.path_edit = QLineEdit()
        self.path_edit.setPlaceholderText("选择视频下载保存路径")
        default_path = app_config.get("download.default_path", "")
        if default_path:
            self.path_edit.setText(default_path)
            self.download_path = default_path
        self.path_edit.setMinimumWidth(300)
        top_row.addWidget(self.path_edit)
        browse_btn = QPushButton("浏览...")
        browse_btn.setObjectName("browse")
        browse_btn.clicked.connect(self.select_path)
        browse_btn.setFixedWidth(80)
        top_row.addWidget(browse_btn)
        top_row.addSpacing(20)
        
        # 并发下载数
        top_row.addWidget(QLabel("并发下载数:"))
        self.thread_spin = QSpinBox()
        self.thread_spin.setRange(1, 10)
        self.thread_spin.setValue(app_config.get("download.max_threads", 3))
        self.thread_spin.setSuffix(" 个")
        self.thread_spin.setFixedWidth(90)
        top_row.addWidget(self.thread_spin)
        top_row.addSpacing(20)
        
        # 文件夹分类
        top_row.addWidget(QLabel("文件夹分类:"))
        self.classify_combo = QComboBox()
        self.classify_combo.addItem("一级类目", "categoryLevel1Name")
        self.classify_combo.addItem("二级类目", "categoryLevel2Name")
        self.classify_combo.addItem("三级类目", "categoryLevel3Name")
        self.classify_combo.addItem("所属行业", "mainIndustry")
        self.classify_combo.addItem("不分类", "none")
        self.classify_combo.setMinimumWidth(130)
        # 从配置读取上次选择（默认一级类目）
        saved_classify = app_config.get("material_download.classify_by", "categoryLevel1Name")
        for i in range(self.classify_combo.count()):
            if self.classify_combo.itemData(i) == saved_classify:
                self.classify_combo.setCurrentIndex(i)
                break
        top_row.addWidget(self.classify_combo)
        top_row.addStretch()
        layout.addRow(top_row)
        
        # === 素材榜特有筛选条件（一行显示） ===
        filter_row = QHBoxLayout()
        
        # 行业筛选
        filter_row.addWidget(QLabel("所属行业:"))
        self.industry_combo = QComboBox()
        self.industry_combo.addItem("不限", "")
        self.industry_combo.addItem("冲调/水饮", "冲调/水饮")
        self.industry_combo.addItem("农资", "农资")
        self.industry_combo.addItem("3C配件", "3C配件")
        self.industry_combo.addItem("男装", "男装")
        self.industry_combo.addItem("女装", "女装")
        self.industry_combo.setMinimumWidth(140)
        filter_row.addWidget(self.industry_combo)
        filter_row.addSpacing(15)
        
        # 体验分筛选
        filter_row.addWidget(QLabel("最低体验分:"))
        self.ue_score_spin = QSpinBox()
        self.ue_score_spin.setRange(0, 100)
        self.ue_score_spin.setValue(0)
        self.ue_score_spin.setSpecialValueText("不限")
        self.ue_score_spin.setFixedWidth(100)
        filter_row.addWidget(self.ue_score_spin)
        filter_row.addSpacing(15)
        
        # 时间筛选
        filter_row.addWidget(QLabel("创建时间:"))
        self.recent_days_combo = QComboBox()
        self.recent_days_combo.addItem("不限", 0)
        self.recent_days_combo.addItem("最近3天", 3)
        self.recent_days_combo.addItem("最近7天", 7)
        self.recent_days_combo.addItem("最近30天", 30)
        self.recent_days_combo.setMinimumWidth(120)
        filter_row.addWidget(self.recent_days_combo)
        filter_row.addSpacing(15)
        
        # 视频时长筛选
        filter_row.addWidget(QLabel("视频时长:"))
        self.duration_combo = QComboBox()
        self.duration_combo.addItem("不限", "")
        self.duration_combo.addItem("0-20秒", "0-20")
        self.duration_combo.addItem("20-40秒", "20-40")
        self.duration_combo.addItem("40-60秒", "40-60")
        self.duration_combo.addItem("60秒以上", "60+")
        self.duration_combo.setMinimumWidth(120)
        filter_row.addWidget(self.duration_combo)
        filter_row.addStretch()
        
        layout.addRow("筛选条件:", filter_row)
        
        # 按钮行
        ctrl_row = QHBoxLayout()
        self.start_btn = QPushButton("开始下载")
        self.start_btn.setStyleSheet("""
            QPushButton { background-color: #2563eb; color: white; }
            QPushButton:hover { background-color: #1d4ed8; }
            QPushButton:disabled { background-color: #6c757d; color: #ffffff; }
        """)
        self.start_btn.clicked.connect(self.on_start_download)
        self.stop_btn = QPushButton("停止任务")
        self.stop_btn.setObjectName("secondary")
        self.stop_btn.setEnabled(False)
        self.stop_btn.clicked.connect(self.stop_task)
        clear_btn = QPushButton("清除日志")
        clear_btn.setObjectName("secondary")
        clear_btn.clicked.connect(self.clear_log)
        ctrl_row.addStretch()
        ctrl_row.addWidget(clear_btn)
        ctrl_row.addWidget(self.stop_btn)
        ctrl_row.addWidget(self.start_btn)
        layout.addRow(ctrl_row)
        
        # 进度与统计
        progress_box = QHBoxLayout()
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(False)
        self.stats_label = QLabel("等待开始…")
        self.stats_label.setStyleSheet("color:#059669; font-weight:600;")
        progress_box.addWidget(self.progress_bar, 3)
        progress_box.addWidget(self.stats_label, 2)
        layout.addRow(progress_box)
        
        # 日志控制台
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setStyleSheet("""
            QTextEdit {
                background-color: #2b2b2b;
                color: #ffffff;
                font-family: 'Consolas', 'Monaco', monospace;
                font-size: 12px;
                border: 1px solid #555;
                border-radius: 6px;
                padding: 8px;
            }
        """)
        log_group = QGroupBox("")
        lg = QVBoxLayout(log_group)
        self.log_text.setMinimumHeight(320)
        lg.addWidget(self.log_text)
        layout.addRow(log_group)
        
    def select_path(self):
        from PyQt5.QtWidgets import QFileDialog
        path = QFileDialog.getExistingDirectory(self, "选择下载文件夹")
        if path:
            self.path_edit.setText(path)
            self.download_path = path
            
    def get_config(self):
        # 保存分类选择到配置
        from config import config as app_config
        classify_by = self.classify_combo.currentData()
        app_config.set("material_download.classify_by", classify_by)
        
        # 构建筛选条件
        filters = {
            "classify_by": classify_by  # 分类字段
        }
        
        # 行业
        industry = self.industry_combo.currentData()
        if industry:
            filters["industry"] = industry
        
        # 体验分
        min_ue_score = self.ue_score_spin.value()
        if min_ue_score > 0:
            filters["min_ue_score"] = min_ue_score
        
        # 创建时间
        recent_days = self.recent_days_combo.currentData()
        if recent_days and recent_days > 0:
            filters["recent_days"] = recent_days
        
        # 视频时长
        duration_range = self.duration_combo.currentData()
        if duration_range and duration_range != "":
            if "-" in duration_range:
                parts = duration_range.split("-")
                filters["min_duration"] = int(parts[0])
                filters["max_duration"] = int(parts[1])
            elif duration_range == "60+":
                filters["min_duration"] = 60
        
        return {
            "download_path": self.download_path,
            "thread_count": self.thread_spin.value(),
            "filters": filters
        }
    
    def on_start_download(self):
        cfg = self.get_config()
        if not cfg.get("download_path"):
            from PyQt5.QtWidgets import QMessageBox
            QMessageBox.warning(self, "警告", "请选择下载路径")
            return
        self.task_started()
        if hasattr(self.main_window, 'start_material_download_from_panel'):
            self.main_window.start_material_download_from_panel(cfg)
            
    def set_task_object(self, task_object):
        self.task_object = task_object

    def stop_task(self):
        """停止素材榜下载任务"""
        if hasattr(self, 'task_object') and self.task_object:
            try:
                self.task_object.stop()
                self.append_log("任务已停止")
            except Exception as e:
                self.append_log(f"ERROR: 停止任务失败: {e}")
        self.stop_btn.setEnabled(False)
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(1)
        
    def task_started(self):
        self.stop_btn.setEnabled(True)
        self.start_btn.setEnabled(False)
        self.stop_btn.setStyleSheet("""
            QPushButton { background-color: #dc3545; color: white; }
            QPushButton:hover { background-color: #c82333; }
        """)
        self.progress_bar.setValue(0)
        self.progress_bar.setRange(0, 0)
        self._load_today_base_downloaded()
        try:
            self.stats_label.setText(f"正在下载… ｜ 今日累计：{self._today_base_downloaded} 个视频")
        except Exception:
            self.stats_label.setText("正在下载…")
            
    def task_finished(self):
        self.stop_btn.setEnabled(False)
        self.stop_btn.setStyleSheet("")
        self.start_btn.setEnabled(True)
        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(1)
        
    def on_progress(self, message: str):
        self.append_log(message)
        try:
            import re
            dl_match = re.search(r"累计下载[：:]?\s*(\d+)\s*个视频", message)
            if dl_match:
                n = int(dl_match.group(1))
                total_today = self._today_base_downloaded + n
                self.stats_label.setText(f"已下载视频数：{n}，并且已分好类目 ｜ 今日累计：{total_today} 个视频")
        except Exception:
            pass
            
    def append_log(self, message: str):
        from datetime import datetime
        timestamp = datetime.now().strftime("%H:%M:%S")
        if "错误" in message or "失败" in message:
            color = "#ff6b6b"
        elif "成功" in message or "完成" in message:
            color = "#00ff00"
        elif "警告" in message:
            color = "#ffaa00"
        elif "开始" in message or "下载" in message:
            color = "#00bfff"
        else:
            color = "#ffffff"
        log_entry = f'<span style="color: {color};">[{timestamp}] {message}</span>'
        self.log_text.append(log_entry)
        cursor = self.log_text.textCursor()
        cursor.movePosition(cursor.End)
        self.log_text.setTextCursor(cursor)
        
    def clear_log(self):
        self.log_text.clear()
        
    def _load_today_base_downloaded(self):
        try:
            from datetime import datetime
            from config import config as app_config
            today = datetime.now().strftime('%Y%m%d')
            d_date = app_config.get("stats.download.date", "")
            d_videos = app_config.get("stats.download.videos", 0)
            if not d_date or d_date != today:
                d_videos = 0
                app_config.set("stats.download.date", today)
                app_config.set("stats.download.videos", 0)
            try:
                d_videos = int(d_videos)
            except Exception:
                d_videos = 0
            self._today_base_downloaded = max(0, d_videos)
        except Exception:
            self._today_base_downloaded = 0


# 已抓取数据列表面板
class GoodsListPanel(QWidget):
    def __init__(self, main_window):
        super().__init__(main_window)
        self.main_window = main_window
        self.setup_ui()

    def setup_ui(self):
        self.setStyleSheet("""
            QWidget { font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif; }
            QPushButton { padding: 6px 12px; border: none; border-radius: 8px; background-color: #3b82f6; color: white; font-weight: 600; }
            QPushButton:hover { background-color: #2563eb; }
            QLineEdit, QDateEdit, QComboBox { padding: 6px 10px; border: 1px solid #e5e7eb; border-radius: 8px; background-color: white; min-height: 28px; }
            QCheckBox { padding-left: 6px; }
            QTableWidget { gridline-color: #e5e7eb; }
        """)
        outer = QVBoxLayout(self)
        outer.setSpacing(8); outer.setContentsMargins(6,6,6,6)

        # 过滤行
        row = QHBoxLayout()
        row.addWidget(QLabel("关键字:"))
        self.keyword_edit = QLineEdit(); self.keyword_edit.setPlaceholderText("商品标题或ID")
        self.keyword_edit.setFixedWidth(220)
        row.addWidget(self.keyword_edit)
        row.addSpacing(8)
        row.addWidget(QLabel("开始日期:"))
        self.start_date = QDateEdit(); self.start_date.setCalendarPopup(True)
        from PyQt5.QtCore import QDate
        self.start_date.setDate(QDate.currentDate().addDays(-7))
        row.addWidget(self.start_date)
        row.addWidget(QLabel("结束日期:"))
        self.end_date = QDateEdit(); self.end_date.setCalendarPopup(True)
        self.end_date.setDate(QDate.currentDate())
        row.addWidget(self.end_date)
        self.all_time_chk = QCheckBox("全部时间")
        self.all_time_chk.stateChanged.connect(self._on_all_time_toggle)
        row.addWidget(self.all_time_chk)
        row.addStretch()
        self.search_btn = QPushButton("搜索")
        self.search_btn.clicked.connect(self.on_search)
        row.addWidget(self.search_btn)
        row.addSpacing(6)
        self.export_scope = QComboBox(); self.export_scope.addItems(["按当前筛选", "全部数据"])
        row.addWidget(self.export_scope)
        self.export_btn = QPushButton("导出Excel")
        self.export_btn.clicked.connect(self.on_export)
        row.addWidget(self.export_btn)
        outer.addLayout(row)

        # 表格
        self.table = QTableWidget(0, 10)
        headers = ["商品ID", "标题", "主图URL", "价格", "佣金率(%)", "赚取金额", "类目", "子类目", "时间范围", "创建时间"]
        self.table.setHorizontalHeaderLabels(headers)
        self.table.setAlternatingRowColors(True)
        header = self.table.horizontalHeader()
        header.setStyleSheet("QHeaderView::section{background:#f3f4f6; font-weight:600; border:1px solid #e5e7eb; padding:6px;}")
        header.setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
        header.setSectionResizeMode(QHeaderView.Interactive)
        header.setStretchLastSection(False)
        header.setMinimumSectionSize(80)
        # 确保ID与标题按内容自适应，允许出现水平滚动
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QHeaderView.ResizeToContents)  # 主图URL列自适应
        # 创建时间列调宽一些
        try:
            header.resizeSection(9, 200)
        except Exception:
            pass
        self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.table.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.table.setWordWrap(False)
        self.table.setTextElideMode(Qt.TextElideMode.ElideNone)
        self.table.verticalHeader().setVisible(False)
        outer.addWidget(self.table)

        # 初始加载
        self.load_data()

    def _on_all_time_toggle(self):
        disabled = self.all_time_chk.isChecked()
        self.start_date.setEnabled(not disabled)
        self.end_date.setEnabled(not disabled)

    def _build_query(self, for_export=False):
        keyword = self.keyword_edit.text().strip()
        where = ["1=1"]
        params = []
        # 关键字搜索：默认使用最近半年
        if keyword:
            where.append("(itemTitle LIKE %s OR relItemId LIKE %s)")
            kw = f"%{keyword}%"; params += [kw, kw]
            from datetime import datetime, timedelta
            end = datetime.now()
            start = end - timedelta(days=180)
            where.append("(createDate BETWEEN %s AND %s)")
            params += [start, end]
        else:
            # 无关键字：可选日期范围
            if not self.all_time_chk.isChecked():
                from datetime import datetime
                sd = self.start_date.date(); ed = self.end_date.date()
                start = datetime(sd.year(), sd.month(), sd.day(), 0, 0, 0)
                end = datetime(ed.year(), ed.month(), ed.day(), 23, 59, 59)
                where.append("(createDate BETWEEN %s AND %s)")
                params += [start, end]

        sql = (
            "SELECT relItemId, itemTitle, itemImgUrl, zkFinalPrice, commissionRate, profitAmount, "
            "categoryName, subCategoryName, rankType, createDate FROM ks_goods_rank "
            f"WHERE {' AND '.join(where)} ORDER BY createDate DESC"
        )
        return sql, params

    def load_data(self):
        try:
            from db_connection import DatabaseConnection
            conn = DatabaseConnection.create_connection()
            try:
                with conn.cursor() as cur:
                    sql, params = self._build_query()
                    cur.execute(sql, params)
                    rows = cur.fetchall()
                    self._fill_table(rows)
            finally:
                conn.close()
        except Exception as e:
            QMessageBox.critical(self, "错误", f"查询失败: {e}")

    def _fill_table(self, rows):
        self.table.setRowCount(0)
        for r in rows:
            row = self.table.rowCount(); self.table.insertRow(row)
            # 依次填充
            for col, val in enumerate(r):
                text = "" if val is None else str(val)
                item = QTableWidgetItem(text)
                # 所有字段居中显示
                item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                # 给长文本设置 tooltip，便于查看完整 URL/标题
                if col in (1, 2):
                    item.setToolTip(text)
                self.table.setItem(row, col, item)
        # 自适应列宽（尤其是ID、标题），允许水平滚动条
        try:
            self.table.resizeColumnToContents(0)
            self.table.resizeColumnToContents(1)
        except Exception:
            pass

    def on_search(self):
        self.load_data()

    def on_export(self):
        try:
            from db_connection import DatabaseConnection
            sql, params = self._build_query(for_export=True)
            # 全部数据导出
            if self.export_scope.currentIndex() == 1:
                sql = (
                    "SELECT relItemId, itemTitle, itemImgUrl, zkFinalPrice, commissionRate, profitAmount, "
                    "categoryName, subCategoryName, rankType, createDate FROM ks_goods_rank ORDER BY createDate DESC"
                )
                params = []
            conn = DatabaseConnection.create_connection()
            try:
                with conn.cursor() as cur:
                    cur.execute(sql, params)
                    rows = cur.fetchall()
            finally:
                conn.close()

            # 导出到Excel（带进度条）
            from openpyxl import Workbook
            from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
            wb = Workbook(); ws = wb.active; ws.title = "商品列表"
            headers = ["商品ID", "标题", "主图URL", "价格", "佣金率(%)", "赚取金额", "类目", "子类目", "时间范围", "创建时间"]
            ws.append(headers)
            # 美化表头
            fill = PatternFill(start_color="4F81BD", end_color="4F81BD", fill_type="solid")
            font = Font(color="FFFFFF", bold=True)
            align = Alignment(horizontal="center", vertical="center")
            border = Border(left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"))
            for cell in ws[1]:
                cell.fill = fill; cell.font = font; cell.alignment = align; cell.border = border
            # 先选择保存路径
            from PyQt5.QtWidgets import QFileDialog
            from datetime import datetime
            default_name = f"商品列表_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
            path, _ = QFileDialog.getSaveFileName(self, "保存Excel", default_name, "Excel 文件 (*.xlsx)")
            if not path:
                return
            # 显示进度条再开始写入
            total = len(rows)
            prog = QDialog(self); prog.setWindowTitle("导出中…")
            v = QVBoxLayout(prog); lab = QLabel(f"正在导出 {total} 条数据…"); v.addWidget(lab)
            bar = QProgressBar(); bar.setRange(0, max(1, total)); bar.setValue(0); v.addWidget(bar)
            prog.setModal(True); prog.resize(360, 100); prog.show()
            from PyQt5.QtWidgets import QApplication
            try:
                # 逐行写入
                for i, r in enumerate(rows, start=1):
                    ws.append([x if x is not None else "" for x in r])
                    if i % 50 == 0 or i == total:
                        bar.setValue(i); QApplication.processEvents()
                # 列宽
                widths = [16, 40, 50, 10, 12, 12, 16, 18, 12, 26]  # 创建时间更宽
                for i, w in enumerate(widths, start=1):
                    ws.column_dimensions[chr(64+i)].width = w
                # 保存
                wb.save(path)
                QMessageBox.information(self, "完成", f"已导出 {len(rows)} 条数据到:\n{path}")
            finally:
                try:
                    prog.close()
                except Exception:
                    pass
        except Exception as e:
            QMessageBox.critical(self, "错误", f"导出失败: {e}")


# Pollinations.AI 图片生成面板
class PollinationsPanel(QWidget):
    # 日志信号：确保所有日志追加都在主线程执行，避免跨线程操作 QTextEdit
    log_message = pyqtSignal(str)
    
    def __init__(self, main_window):
        super().__init__(main_window)
        self.main_window = main_window
        self.generator = None
        self.save_folder = ""
        self.is_generating = False
        # 日志信号连接到实际更新 UI 的槽，确保在主线程操作 QTextEdit
        self.log_message.connect(self._append_log_main_thread)
        self.setup_ui()
    
    def setup_ui(self):
        # 美化样式 - 更现代的设计
        self.setStyleSheet("""
            QWidget { 
                font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', Arial, sans-serif;
                background-color: #f8fafc;
            }
            QGroupBox { 
                font-weight: 600; 
                border: 2px solid #e0e7ff; 
                border-radius: 12px; 
                margin-top: 12px; 
                padding-top: 16px; 
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #f8fafc);
            }
            QGroupBox::title { 
                subcontrol-origin: margin; 
                left: 14px; 
                padding: 0 8px; 
                color: #1e40af; 
                font-size: 14px;
            }
            QLabel { 
                color: #1f2937; 
                font-size: 13px; 
                font-weight: 500;
            }
            QTextEdit { 
                border: 2px solid #e5e7eb; 
                border-radius: 10px; 
                padding: 12px; 
                background-color: #ffffff;
                font-size: 13px;
                selection-background-color: #dbeafe;
            }
            QTextEdit:focus {
                border: 2px solid #60a5fa;
                background-color: #ffffff;
            }
            QComboBox, QSpinBox, QLineEdit {
                padding: 8px 12px; 
                border: 2px solid #e5e7eb; 
                border-radius: 10px; 
                background-color: #ffffff;
                min-width: 120px; 
                min-height: 32px; 
                font-size: 13px;
                font-weight: 500;
            }
            QComboBox:hover, QSpinBox:hover, QLineEdit:hover {
                border: 2px solid #93c5fd;
            }
            QComboBox:focus, QSpinBox:focus, QLineEdit:focus { 
                border: 2px solid #3b82f6;
                background-color: #ffffff;
            }
            QSpinBox { padding-right: 30px; }
            QPushButton { 
                padding: 10px 20px; 
                border: none; 
                border-radius: 10px; 
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3b82f6, stop:1 #2563eb);
                color: white; 
                font-weight: 600;
                font-size: 14px;
            }
            QPushButton:hover { 
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2563eb, stop:1 #1d4ed8);
            }
            QPushButton:pressed {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1e40af, stop:1 #1e3a8a);
            }
            QPushButton:disabled { 
                background: #9ca3af;
            }
            QPushButton#secondary { 
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #f3f4f6, stop:1 #e5e7eb);
                color: #374151;
                border: 2px solid #d1d5db;
            }
            QPushButton#secondary:hover { 
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #e5e7eb, stop:1 #d1d5db);
                border: 2px solid #9ca3af;
            }
            QCheckBox, QRadioButton { 
                spacing: 6px; 
                padding: 6px 8px; 
                border-radius: 8px;
                font-weight: 500;
            }
            QCheckBox:hover, QRadioButton:hover {
                background-color: #f0f9ff;
            }
            QRadioButton::indicator { 
                width: 18px; 
                height: 18px; 
                border: 2px solid #94a3b8; 
                border-radius: 9px; 
                background: white; 
            }
            QRadioButton::indicator:checked { 
                background-color: #3b82f6; 
                border-color: #3b82f6; 
            }
        """)
        
        layout = QVBoxLayout(self)
        layout.setSpacing(16)
        layout.setContentsMargins(20, 20, 20, 20)
        
        # 配置表单
        form_layout = QFormLayout()
        form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
        form_layout.setHorizontalSpacing(20)
        form_layout.setVerticalSpacing(14)
        # 让字段能够扩展
        form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
        
        # 生成服务选择 - 使用单选框
        service_widget = QWidget()
        service_row = QHBoxLayout(service_widget)
        service_row.setContentsMargins(0, 0, 0, 0)
        service_row.setSpacing(20)
        
        self.service_button_group = QButtonGroup(self)
        
        self.pollinations_radio = QRadioButton("Pollinations.AI（无需API Key）")
        self.pollinations_radio.setChecked(True)
        self.pollinations_radio.setStyleSheet("""
            QRadioButton {
                spacing: 8px;
                padding: 8px 12px;
                font-size: 14px;
                font-weight: 500;
                color: #1f2937;
            }
        """)
        self.service_button_group.addButton(self.pollinations_radio, 0)
        service_row.addWidget(self.pollinations_radio)
        
        self.siliconflow_radio = QRadioButton("硅基流动（需要API Key）")
        self.siliconflow_radio.setStyleSheet("""
            QRadioButton {
                spacing: 8px;
                padding: 8px 12px;
                font-size: 14px;
                font-weight: 500;
                color: #1f2937;
            }
        """)
        self.service_button_group.addButton(self.siliconflow_radio, 1)
        service_row.addWidget(self.siliconflow_radio)
        
        # API Key 配置按钮（仅对 SiliconFlow）
        self.api_key_btn = QPushButton("🔑 配置 API Keys")
        self.api_key_btn.clicked.connect(self.show_api_key_dialog)
        self.api_key_btn.setFixedHeight(38)
        self.api_key_btn.setStyleSheet("""
            QPushButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3b82f6, stop:1 #2563eb);
                color: white;
                border: none;
                border-radius: 10px;
                font-size: 14px;
                font-weight: 600;
                padding: 8px 16px;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2563eb, stop:1 #1d4ed8);
            }
            QPushButton:pressed {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1e40af, stop:1 #1e3a8a);
            }
        """)
        service_row.addWidget(self.api_key_btn)
        self.api_key_btn.hide()  # 初始隐藏
        
        service_row.addStretch()
        
        self.service_button_group.buttonClicked.connect(self.on_service_changed)
        form_layout.addRow("", service_widget)
        
        # 保存 API Keys 到内部变量
        from config import config as app_config
        self.saved_api_keys = app_config.get("siliconflow.api_keys", [])
        
        # 保存位置选择
        # 使用单独的容器控件，方便控制整体宽度（只占整体宽度的三分之一左右）
        self.folder_widget = QWidget()
        folder_row = QHBoxLayout(self.folder_widget)
        folder_row.setContentsMargins(0, 0, 0, 0)
        folder_row.setSpacing(10)

        # 只作为路径显示，不再强制 1000px 宽度，交给上层容器控制
        self.folder_edit = QLineEdit()
        self.folder_edit.setPlaceholderText("选择图片保存位置")
        from config import config as app_config
        default_folder = app_config.get("pollinations.save_folder", "./pollinations_images")
        self.folder_edit.setText(default_folder)
        self.save_folder = default_folder
        folder_row.addWidget(self.folder_edit, 1)  # 使用 stretch factor 填满容器内部宽度

        browse_btn = QPushButton("📁 浏览")
        browse_btn.clicked.connect(self.select_folder)
        browse_btn.setFixedWidth(100)
        browse_btn.setStyleSheet("""
            QPushButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #667eea, stop:1 #5a67d8);
                color: white;
                border: none;
                border-radius: 10px;
                font-size: 14px;
                font-weight: 600;
                padding: 8px 16px;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a67d8, stop:1 #4c51bf);
            }
            QPushButton:pressed {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4c51bf, stop:1 #434190);
            }
        """)
        folder_row.addWidget(browse_btn)

        # 让“保存位置”这一行最多只占整体宽度的大约三分之一，右侧留出空白
        from PyQt5.QtWidgets import QSizePolicy
        self.folder_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        # 初始时按当前面板宽度的 1/3 做一个上限，后续在 resizeEvent 中再动态调整
        try:
            self.folder_widget.setMaximumWidth(max(400, int(self.width() * (1.0 / 3))))
        except Exception:
            self.folder_widget.setMaximumWidth(600)
        
        # 关键词输入框仅用于内部存储，不放到布局里以免占用空间
        self.prompt_edit = QTextEdit(self)
        self.prompt_edit.setPlaceholderText("输入需要生成的图片关键词，每行一个关键词\n例如：\ngolden sunflower, nature photography\nbeautiful landscape at sunset\nmountain scenery, high quality")
        self.prompt_edit.setMinimumWidth(1000)
        self.prompt_edit.setFixedHeight(180)
        self.prompt_edit.hide()
        
        # 参数设置行（控件保留用于内部读取，但不展示在界面上）
        params_row = QHBoxLayout()

        # 随机种子（隐藏控件，只用于内部默认值）
        self.seed_spin = QSpinBox()
        self.seed_spin.setRange(1, 999999)
        self.seed_spin.setValue(10000)
        self.seed_spin.setFixedWidth(100)
        self.seed_spin.hide()

        # 图片分辨率（宽，高）隐藏，仅使用默认 576×1024
        self.width_spin = QSpinBox()
        self.width_spin.setRange(256, 2048)
        self.width_spin.setValue(576)  # 默认竖屏宽度
        self.width_spin.setSuffix(" px")
        self.width_spin.setFixedWidth(100)
        self.width_spin.setReadOnly(True)
        self.width_spin.hide()

        self.height_spin = QSpinBox()
        self.height_spin.setRange(256, 2048)
        self.height_spin.setValue(1024)  # 默认竖屏高度
        self.height_spin.setSuffix(" px")
        self.height_spin.setFixedWidth(100)
        self.height_spin.setReadOnly(True)
        self.height_spin.hide()

        # 每个关键词生成张数（自动模式固定为 1，手动模式内部可改，这里不展示）
        self.count_spin = QSpinBox()
        self.count_spin.setRange(1, 100)
        self.count_spin.setValue(1)
        self.count_spin.setSuffix(" 张")
        self.count_spin.setFixedWidth(90)
        self.count_spin.setToolTip("手动模式：每个关键词生成的图片数量\n自动模式：固定为1张/关键词（由目标总张数控制）")
        self.count_spin.hide()

        # 不再向 form_layout 添加这一行，从界面上完全移除
        # params_row 仍然存在以保持代码结构，但为空行
        # 不向 form_layout 添加任何控件，避免产生额外空白
        
        # 图片模型选择 + 自动生成（合并一行）
        model_auto_row = QHBoxLayout()
        
        # 模型选择
        self.model_label = QLabel("图片模型/风格:")
        model_auto_row.addWidget(self.model_label)
        self.model_combo = QComboBox()
        self.model_combo.addItems(PollinationsGenerator.get_available_styles()[:30])  # 显示前30个常用风格
        # 调宽图片模型下拉框，便于在 Windows / macOS 上完整显示长模型名称
        # 使用内容自适应 + 最小宽度，避免被布局压缩得太窄
        self.model_combo.setMinimumWidth(420)
        try:
            from PyQt5.QtWidgets import QComboBox as _QC
            self.model_combo.setSizeAdjustPolicy(_QC.AdjustToContents)
            self.model_combo.setMinimumContentsLength(30)  # 至少显示较长前缀
        except Exception:
            pass
        model_auto_row.addWidget(self.model_combo)
        model_auto_row.addSpacing(20)
        
        # 自动轮换免费模型复选框（仅对硅基流动）
        self.auto_rotate_models_checkbox = QCheckBox("🔄 自动轮换免费模型")
        self.auto_rotate_models_checkbox.setChecked(True)  # 默认勾选
        self.auto_rotate_models_checkbox.setToolTip("勾选后会在 Kolors 和 FLUX.1-schnell 之间自动轮换，\n充分利用两个模型的免费额度（每天800张）")
        self.auto_rotate_models_checkbox.setStyleSheet("""
            QCheckBox {
                spacing: 8px;
                padding: 6px 10px;
                font-size: 13px;
                font-weight: 500;
                color: #059669;
            }
            QCheckBox:hover {
                background-color: #f0fdf4;
                border-radius: 6px;
            }
        """)
        self.auto_rotate_models_checkbox.hide()  # 初始隐藏，只在选择硅基流动时显示
        self.auto_rotate_models_checkbox.stateChanged.connect(self.on_auto_rotate_changed)
        model_auto_row.addWidget(self.auto_rotate_models_checkbox)
        
        # 自动生成选项（逻辑保留，但复选框不再显示，默认视为启用自动模式）
        self.auto_gen_checkbox = QCheckBox("自动生成随机关键词")
        self.auto_gen_checkbox.setChecked(True)
        self.auto_gen_checkbox.stateChanged.connect(self.on_auto_gen_changed)
        self.auto_gen_checkbox.hide()

        # 是否使用代理下载图片（ShadowsocksX，本地 SOCKS5，默认关闭）
        # 文案改为更简短的“开启代理”，含义保持不变
        self.use_proxy_checkbox = QCheckBox("开启代理")
        self.use_proxy_checkbox.setToolTip("通过本地 ShadowsocksX 的 SOCKS5 代理 127.0.0.1:1086 下载 Pollinations / SiliconFlow 图片")
        try:
            from config import config as app_config
            use_proxy = app_config.get("network.proxy_enabled", False)
        except Exception:
            use_proxy = False
        self.use_proxy_checkbox.setChecked(use_proxy)
        self.use_proxy_checkbox.stateChanged.connect(self.on_proxy_changed)
        model_auto_row.addWidget(self.use_proxy_checkbox)

        # 目标总张数（不再对用户显示，只使用内部配置）
        self.total_images_label = QLabel("目标总张数:")
        self.total_images_label.hide()
        self.total_images_spin = QSpinBox()
        self.total_images_spin.setRange(1, 100000)
        # 从配置加载上次设置，默认 100000；
        # 如果旧配置里小于 100000（例如 3000），强制提升到 100000
        try:
            from config import config as app_config
            saved_total = app_config.get("pollinations.target_total", 100000)
            if isinstance(saved_total, dict):
                saved_total = saved_total.get("value", 100000)
            saved_total = int(saved_total)
        except Exception:
            saved_total = 100000
        if saved_total < 100000:
            saved_total = 100000
        self.total_images_spin.setValue(saved_total)
        self.total_images_spin.setSuffix(" 张")
        self.total_images_spin.setFixedWidth(130)
        # 不加入布局，只在内部读取目标总张数
        self.total_images_spin.hide()

        # 随机关键词数量（内部使用，不显示在界面上，固定默认 300）
        self.auto_count_spin = QSpinBox()
        self.auto_count_spin.setRange(1, 10000)
        self.auto_count_spin.setValue(300)
        self.auto_count_spin.setSuffix(" 张")
        self.auto_count_spin.setFixedWidth(100)
        self.auto_count_spin.setEnabled(True)
        self.auto_count_spin.hide()
        # 数量改变时也重新生成关键词（主要用于以后如果需要开放该设置）
        self.auto_count_spin.valueChanged.connect(self.on_auto_count_changed)
        
        # 自动更新提示标签也隐藏
        auto_refresh_label = QLabel("🔄 每30分钟自动更新")
        auto_refresh_label.setStyleSheet("""
            QLabel {
                color: #6b7280;
                font-size: 11px;
                font-style: italic;
                padding: 4px 8px;
            }
        """)
        auto_refresh_label.hide()

        model_auto_row.addStretch()

        # 将“保存位置”和“图片模型/风格 + 开启代理”放在同一行
        row_container = QWidget()
        row_layout = QHBoxLayout(row_container)
        row_layout.setContentsMargins(0, 0, 0, 0)
        row_layout.setSpacing(24)

        # 左侧：保存位置选择（约占整行 1/3 宽度）
        row_layout.addWidget(self.folder_widget, 1)

        # 右侧：图片模型/风格与代理开关
        model_auto_widget = QWidget()
        model_auto_widget.setLayout(model_auto_row)
        row_layout.addWidget(model_auto_widget, 2)

        form_layout.addRow("", row_container)
        
        layout.addLayout(form_layout)
        
        # 按钮区域
        button_row = QHBoxLayout()
        button_row.setSpacing(16)
        button_row.addStretch()
        
        self.generate_btn = QPushButton("🚀 开始生成")
        self.generate_btn.clicked.connect(self.start_generation)
        self.generate_btn.setMinimumWidth(180)
        self.generate_btn.setMinimumHeight(50)
        self.generate_btn.setStyleSheet("""
            QPushButton {
                padding: 12px 28px;
                border: none;
                border-radius: 12px;
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #10b981, stop:1 #059669);
                color: white;
                font-weight: 700;
                font-size: 16px;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #059669, stop:1 #047857);
            }
            QPushButton:pressed {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #047857, stop:1 #065f46);
            }
            QPushButton:disabled {
                background: #9ca3af;
            }
        """)
        button_row.addWidget(self.generate_btn)
        
        self.stop_btn = QPushButton("停止")
        self.stop_btn.clicked.connect(self.stop_generation)
        self.stop_btn.setEnabled(False)
        self.stop_btn.setMinimumWidth(160)
        self.stop_btn.setMinimumHeight(50)
        # 保存原始样式，后面会动态更改
        self.stop_btn_disabled_style = """
            QPushButton {
                padding: 12px 28px;
                border: none;
                border-radius: 12px;
                background: #e5e7eb;
                color: #9ca3af;
                font-weight: 600;
                font-size: 16px;
            }
        """
        self.stop_btn_enabled_style = """
            QPushButton {
                padding: 12px 28px;
                border: none;
                border-radius: 12px;
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ef4444, stop:1 #dc2626);
                color: white;
                font-weight: 700;
                font-size: 16px;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #dc2626, stop:1 #b91c1c);
            }
            QPushButton:pressed {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #b91c1c, stop:1 #991b1b);
            }
        """
        self.stop_btn.setStyleSheet(self.stop_btn_disabled_style)
        button_row.addWidget(self.stop_btn)
        button_row.addStretch()
        
        layout.addLayout(button_row)
        
        # 日志控制台区域
        log_container = QWidget()
        log_container.setStyleSheet("""
            QWidget {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1e293b, stop:1 #0f172a);
                border-radius: 12px;
                padding: 12px;
            }
        """)
        log_container_layout = QVBoxLayout(log_container)
        log_container_layout.setSpacing(0)
        log_container_layout.setContentsMargins(0, 0, 0, 0)
        
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        # 让日志控件在 Windows 下尽量填满容器，去掉多余上下空白
        self.log_text.setMinimumHeight(220)  # 稍大一点的最小高度
        # 不再设置最大高度，让其可以随容器一起伸展
        # 限制最多保留的日志行数，超过会自动丢弃最早的行，避免内存增长
        try:
            self.log_text.document().setMaximumBlockCount(100)  # 保留最近100行
        except Exception:
            pass
        self.log_text.setStyleSheet("""
            QTextEdit {
                background-color: #0f172a;
                color: #e2e8f0;
                font-family: 'SF Mono', 'Consolas', 'Monaco', 'Menlo', monospace;
                font-size: 13px;
                border: 2px solid #1e293b;
                border-radius: 10px;
                padding: 12px;
                selection-background-color: #334155;
            }
            QScrollBar:vertical {
                background: #1e293b;
                width: 12px;
                border-radius: 6px;
            }
            QScrollBar::handle:vertical {
                background: #475569;
                border-radius: 6px;
                min-height: 30px;
            }
            QScrollBar::handle:vertical:hover {
                background: #64748b;
            }
        """)
        # 让日志控件在容器内占满可用空间
        log_container_layout.addWidget(self.log_text, 1)
        layout.addWidget(log_container, 1)
        
        # 定时清空日志，防止长期运行堆积（每10分钟一次）
        self.log_auto_clear_timer = QTimer(self)
        self.log_auto_clear_timer.timeout.connect(self._periodic_log_cleanup)
        self.log_auto_clear_timer.start(10 * 60 * 1000)
        
        # 初始化：默认显示随机关键词（必须在log_text创建后调用）
        self.on_auto_gen_changed()
        
        # 设置定时器，每30分钟自动更新关键词
        self.auto_refresh_timer = QTimer(self)
        self.auto_refresh_timer.timeout.connect(self.auto_refresh_keywords)
        self.auto_refresh_timer.start(30 * 60 * 1000)  # 30分钟 = 1800000毫秒
    
    def select_folder(self):
        # 使用系统原生对话框（已在 main() 中允许原生对话框）
        from PyQt5.QtWidgets import QFileDialog
        default_dir = self.save_folder or os.path.expanduser("~")
        folder = QFileDialog.getExistingDirectory(
            self,
            "选择图片保存文件夹",
            default_dir,
            QFileDialog.ShowDirsOnly
        )
        if folder:
            self.folder_edit.setText(folder)
            self.save_folder = folder
            from config import config as app_config
            app_config.set("pollinations.save_folder", folder)
    
    def show_api_key_dialog(self):
        """显示 API Key 配置对话框"""
        from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
        
        dialog = QDialog(self)
        dialog.setWindowTitle("配置硅基流动 API Keys")
        dialog.setMinimumWidth(600)
        dialog.setMinimumHeight(400)
        
        layout = QVBoxLayout(dialog)
        layout.setSpacing(16)
        layout.setContentsMargins(20, 20, 20, 20)
        
        # 说明文字
        info_label = QLabel("🔑 请输入硅基流动 API Key，每行一个\n限流时会自动轮换使用多个 API Key")
        info_label.setStyleSheet("font-size: 13px; color: #374151; padding: 10px; background: #f0f9ff; border-radius: 8px;")
        layout.addWidget(info_label)
        
        # 多行文本输入
        api_key_edit = QTextEdit()
        api_key_edit.setPlaceholderText("例如：\nsk-xxxxxxxxxxxxx\nsk-yyyyyyyyyyyyy\nsk-zzzzzzzzzzzzz")
        api_key_edit.setStyleSheet("""
            QTextEdit {
                border: 2px solid #d1d5db;
                border-radius: 8px;
                padding: 10px;
                font-family: 'Courier New', monospace;
                font-size: 12px;
                background-color: white;
            }
            QTextEdit:focus {
                border-color: #3b82f6;
            }
        """)
        # 加载保存的 API Keys
        if self.saved_api_keys and isinstance(self.saved_api_keys, list):
            api_key_edit.setPlainText("\n".join(self.saved_api_keys))
        layout.addWidget(api_key_edit)
        
        # 按钮行
        button_row = QHBoxLayout()
        button_row.addStretch()
        
        # 验证按钮
        validate_btn = QPushButton("验证")
        validate_btn.setFixedWidth(100)
        validate_btn.setFixedHeight(38)
        validate_btn.setStyleSheet("""
            QPushButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #10b981, stop:1 #059669);
                color: white;
                border: none;
                border-radius: 8px;
                font-size: 14px;
                font-weight: 600;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #059669, stop:1 #047857);
            }
        """)
        validate_btn.clicked.connect(lambda: self.validate_api_key_from_dialog(api_key_edit.toPlainText()))
        button_row.addWidget(validate_btn)
        
        # 保存按钮
        save_btn = QPushButton("保存")
        save_btn.setFixedWidth(100)
        save_btn.setFixedHeight(38)
        save_btn.setStyleSheet("""
            QPushButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3b82f6, stop:1 #2563eb);
                color: white;
                border: none;
                border-radius: 8px;
                font-size: 14px;
                font-weight: 600;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2563eb, stop:1 #1d4ed8);
            }
        """)
        def save_and_close():
            api_keys_text = api_key_edit.toPlainText().strip()
            api_keys = [key.strip() for key in api_keys_text.split("\n") if key.strip()]
            if api_keys:
                from config import config as app_config
                app_config.set("siliconflow.api_keys", api_keys)
                self.saved_api_keys = api_keys
                self.append_log(f"✓ 已保存 {len(api_keys)} 个 API Key")
            dialog.accept()
        save_btn.clicked.connect(save_and_close)
        button_row.addWidget(save_btn)
        
        # 取消按钮
        cancel_btn = QPushButton("取消")
        cancel_btn.setFixedWidth(100)
        cancel_btn.setFixedHeight(38)
        cancel_btn.setStyleSheet("""
            QPushButton {
                background: #e5e7eb;
                color: #374151;
                border: none;
                border-radius: 8px;
                font-size: 14px;
                font-weight: 600;
            }
            QPushButton:hover {
                background: #d1d5db;
            }
        """)
        cancel_btn.clicked.connect(dialog.reject)
        button_row.addWidget(cancel_btn)
        
        layout.addLayout(button_row)
        
        dialog.exec_()
    
    def validate_api_key_from_dialog(self, api_keys_text):
        """从对话框验证 API Keys"""
        api_keys_text = api_keys_text.strip()
        if not api_keys_text:
            self.append_log("⚠ 请输入至少一个 API Key")
            return
        
        # 解析多个 API Keys
        api_keys = [key.strip() for key in api_keys_text.split("\n") if key.strip()]
        if not api_keys:
            self.append_log("⚠ 请输入有效的 API Key")
            return
        
        self.append_log(f"正在验证 {len(api_keys)} 个 API Key...")
        
        # 在后台线程验证
        import threading
        def validate_thread():
            valid_keys = []
            invalid_keys = []
            
            for i, api_key in enumerate(api_keys, 1):
                self.append_log(f"验证第 {i}/{len(api_keys)} 个...")
                is_valid = SiliconFlowGenerator.validate_api_key(api_key)
                if is_valid:
                    valid_keys.append(api_key)
                    self.append_log(f"✓ 第 {i} 个有效")
                else:
                    invalid_keys.append(api_key)
                    self.append_log(f"✗ 第 {i} 个无效")
            
            if valid_keys:
                self.append_log(f"✓ 验证完成：{len(valid_keys)} 个有效，{len(invalid_keys)} 个无效")
            else:
                self.append_log("✗ 所有 API Key 都无效")
        
        thread = threading.Thread(target=validate_thread, daemon=True)
        thread.start()
    
    def on_auto_rotate_changed(self):
        """自动轮换免费模型状态变化"""
        if self.auto_rotate_models_checkbox.isChecked():
            # 禁用模型下拉框
            self.model_combo.setEnabled(False)
            # 默认选中第一个免费模型
            self.model_combo.setCurrentIndex(0)
            self.append_log("🔄 已开启自动轮换免费模型（Kolors ⇄ FLUX.1-schnell）")
        else:
            # 启用模型下拉框
            self.model_combo.setEnabled(True)
            self.append_log("ℹ️ 已关闭自动轮换，可手动选择模型")
    
    def on_service_changed(self, button=None):
        """切换生成服务"""
        is_siliconflow = self.siliconflow_radio.isChecked()
        
        if is_siliconflow:
            # 显示 API Key 配置按钮
            self.api_key_btn.show()
            # 显示自动轮换复选框
            self.auto_rotate_models_checkbox.show()
            # 更改模型列表
            self.model_combo.clear()
            self.model_combo.addItems(SiliconFlowGenerator.get_available_models())
            self.model_label.setText("图片模型:")
            # 如果自动轮换已勾选，禁用模型下拉框
            if self.auto_rotate_models_checkbox.isChecked():
                self.model_combo.setEnabled(False)
            else:
                self.model_combo.setEnabled(True)
            # 加载保存的 API Keys
            from config import config as app_config
            self.saved_api_keys = app_config.get("siliconflow.api_keys", [])
        else:
            # Pollinations - 隐藏 API Key 配置按钮
            self.api_key_btn.hide()
            # 隐藏自动轮换复选框
            self.auto_rotate_models_checkbox.hide()
            # 更改模型列表
            self.model_combo.clear()
            self.model_combo.addItems(PollinationsGenerator.get_available_styles()[:30])
            self.model_label.setText("图片模型/风格:")
            # 确保模型下拉框启用
            self.model_combo.setEnabled(True)
    
    def on_auto_count_changed(self):
        """当数量改变时重新生成关键词"""
        if self.auto_gen_checkbox.isChecked():
            self.generate_and_display_random_prompts()
        
    def on_proxy_changed(self, state=None):
        """切换是否使用代理下载图片，并写入配置。"""
        try:
            from config import config as app_config
            enabled = self.use_proxy_checkbox.isChecked()
            app_config.set("network.proxy_enabled", enabled)
            if enabled:
                self.append_log("🌐 已启用代理下载图片（ShadowsocksX SOCKS5）")
            else:
                self.append_log("🌐 已关闭代理下载图片，改为直连")
        except Exception:
            pass

    def on_auto_gen_changed(self):
        is_checked = self.auto_gen_checkbox.isChecked()
        self.auto_count_spin.setEnabled(is_checked)
        self.prompt_edit.setEnabled(not is_checked)
        
        # 自动模式下：强制每个关键词生成1张，并禁用该控件
        # 手动模式下：允许用户自定义每个关键词生成张数
        if is_checked:
            self.count_spin.setValue(1)
            self.count_spin.setEnabled(False)
            self.append_log("🤖 已切换到自动模式：每个关键词生成1张，由目标总张数控制（点击“开始生成”后自动刷新随机关键词）")
        else:
            self.count_spin.setEnabled(True)
            self.append_log("✏️ 已切换到手动模式：可自定义每个关键词生成的图片数量")
            self.prompt_edit.clear()
            self.prompt_edit.setPlaceholderText("输入需要生成的图片关键词，每行一个关键词\n例如：\ngolden sunflower, nature photography\nbeautiful landscape at sunset\nmountain scenery, high quality")
    
    def generate_and_display_random_prompts(self):
        """生成随机关键词并显示在输入框中"""
        try:
            count = self.auto_count_spin.value()
            # 使用真随机种子，确保每台电脑生成的都不同
            import random
            import time
            import os
            # 使用多个随机源确保真随机：时间戳 + 进程号 + 随机数 + 系统随机
            true_random_seed = int(time.time() * 1000000) + os.getpid() + random.randint(1, 999999) + int.from_bytes(os.urandom(4), 'big')
            
            # 创建临时生成器
            temp_gen = PollinationsGenerator(self.save_folder or "./temp")
            # 不传入seed参数，让其使用真随机
            prompts = temp_gen.generate_random_prompts("beautiful scenery", count, seed=None)
            
            # 显示在输入框
            self.prompt_edit.setPlainText("\n".join(prompts))
            from datetime import datetime
            current_time = datetime.now().strftime("%H:%M:%S")
            self.append_log(f"✅ [{current_time}] 已生成 {len(prompts)} 个随机关键词（真随机）")
        except Exception as e:
            self.append_log(f"生成随机关键词失败: {e}")
    
    def auto_refresh_keywords(self):
        """自动刷新关键词（每30分钟调用）"""
        if self.auto_gen_checkbox.isChecked():
            self.append_log("🔄 自动更新关键词...")
            self.generate_and_display_random_prompts()
    
    def _periodic_log_cleanup(self):
        """定时清空日志，避免长时间运行导致卡顿"""
        try:
            self.log_text.clear()
        except Exception:
            pass

    def resizeEvent(self, event):  # type: ignore[override]
        """在窗口大小变化时，动态调整“保存位置”一行的最大宽度，让其大约占整体一半。"""
        try:
            super().resizeEvent(event)
        except Exception:
            pass
        try:
            if hasattr(self, "folder_widget") and self.folder_widget is not None:
                self.folder_widget.setMaximumWidth(max(400, int(self.width() * 0.5)))
        except Exception:
            # 容错：即使这里出错也不要影响主流程
            pass
        
    def append_log(self, message: str):
        """线程安全的日志入口：在任何线程里都可以调用。
        实际 UI 更新通过 log_message 信号在主线程执行。
        """
        try:
            self.log_message.emit(message)
        except Exception:
            # 极端情况下（对象正在析构等）直接忽略
            pass

    def _append_log_main_thread(self, message: str):
        """真正执行 QTextEdit 更新的槽函数（只在主线程运行）。"""
        from datetime import datetime
        timestamp = datetime.now().strftime("%H:%M:%S")
        
        # 优化颜色：限流、失败等信息使用柔和的颜色，不再显眼
        if "✓" in message or "成功" in message or "完成" in message or "已保存" in message:
            color = "#00ff00"  # 绿色 - 成功
        elif "限流" in message or "429" in message or "502" in message or "503" in message:
            color = "#aaaaaa"  # 灰色 - 限流/服务器错误，不显眼
        elif "失败" in message or "跳过" in message:
            color = "#999999"  # 淡灰色 - 失败，不显眼
        elif "警告" in message or "✗" in message or "错误" in message:
            color = "#ccaa66"  # 柔和的黄色 - 警告
        elif "开始" in message or "生成" in message or "🔄" in message:
            color = "#00bfff"  # 蓝色 - 任务开始
        elif "⛔" in message:
            color = "#ff9966"  # 柔和的橙色 - 停止
        else:
            color = "#ffffff"  # 白色 - 普通信息
        
        log_entry = f'<span style="color: {color};">[{timestamp}] {message}</span>'
        self.log_text.append(log_entry)
        cursor = self.log_text.textCursor()
        cursor.movePosition(cursor.End)
        self.log_text.setTextCursor(cursor)
        # 立即刷新
        QApplication.processEvents()

    def _run_pollinations_auto_generation(self, prompts, width: int, height: int,
                                          seed: int, model: str,
                                          images_per_prompt: int, target_total: int):
        """Pollinations 自动随机关键词模式：持续生成直到达到目标总数。

        - 使用当前文本框中的随机关键词作为初始批次
        - 用完一批后自动生成新的一批随机关键词
        - 一直生成直到累计达到 target_total 或用户点击“停止”
        """
        import random

        # 如果当前没有关键词，先补一批，避免空跑
        prompts = list(prompts or [])
        if not prompts:
            try:
                batch_size = max(1, self.auto_count_spin.value())
            except Exception:
                batch_size = 300
            try:
                self.append_log("当前没有随机关键词，自动生成一批...")
                prompts = self.generator.generate_random_prompts("beautiful scenery", batch_size, seed=None)
            except Exception as e:
                self.append_log(f"生成随机关键词失败: {e}")
                return

        # 运行时参数
        try:
            batch_size = max(1, self.auto_count_spin.value())
        except Exception:
            batch_size = 300
        images_per_prompt = max(1, int(images_per_prompt) if images_per_prompt is not None else 1)

        generated = 0
        idx = 0

        self.append_log(f"Pollinations 自动模式：目标 {target_total} 张，初始随机关键词 {len(prompts)} 条")

        # 说明：
        # - 不再依赖 generate_image 的返回值来统计成功数量
        # - 统一在 progress_callback 收到 "✓ 图片已保存:" 日志时自增 generated
        #   这样可以避免极端情况下返回值为 None 但已经成功写盘的计数误差
        while self.is_generating and generated < target_total:
            # 如果当前这批关键词用完了，就自动再生成一批新的
            if idx >= len(prompts):
                try:
                    self.append_log("🔄 随机关键词已用完，自动生成新的一批...")
                    prompts = self.generator.generate_random_prompts("beautiful scenery", batch_size, seed=None)
                    idx = 0
                    if not prompts:
                        self.append_log("新生成的随机关键词为空，任务结束")
                        break
                except Exception as e:
                    self.append_log(f"生成随机关键词失败: {e}")
                    break

            # 取当前关键词
            prompt = prompts[idx]
            idx += 1

            # 每个关键词可以生成多张图片，但不会超过目标总数
            for _ in range(images_per_prompt):
                if not self.is_generating or generated >= target_total:
                    break

                current_seed = random.randint(1, 999999)

                def _progress_cb(msg, _self=self):
                    nonlocal generated
                    # 复用统一的日志输出逻辑
                    _self.append_log(msg)
                    # 仅当底层生成器明确输出 "✓ 图片已保存:" 时，才认为本次成功
                    if msg.startswith("✓ 图片已保存:"):
                        generated += 1
                        _self.append_log(f"✓ [{generated}/{target_total}] 已保存")

                # 这里不再依赖返回值做计数，是否成功完全由 progress 回调判断
                self.generator.generate_image(
                    prompt=prompt,
                    width=width,
                    height=height,
                    seed=current_seed,
                    model=model,
                    progress_callback=_progress_cb,
                )

        self.append_log(f"任务结束：累计生成 {generated}/{target_total} 张图片")

    def _run_siliconflow_auto_generation(self, prompts, width: int, height: int,
                                          model: str,
                                          images_per_prompt: int, target_total: int,
                                          num_inference_steps: int = 20,
                                          guidance_scale: float = 7.5,
                                          auto_rotate: bool = False):
        """硅基流动自动随机关键词模式：持续生成直到达到目标总数。

        逻辑与 Pollinations 自动模式类似，只是调用 SiliconFlow 的 generate_image。
        """
        import random

        prompts = list(prompts or [])
        if not prompts:
            # 如果当前没有关键词，直接返回
            self.append_log("没有可用的关键词，无法开始生成任务")
            return

        # 自动模式下不再关心 prompts 的总数，只按目标总数控制
        generated = 0
        idx = 0
        total_keywords = len(prompts)
        
        # 自动轮换相关变量
        if auto_rotate:
            free_models = ["Kwai-Kolors/Kolors", "black-forest-labs/FLUX.1-schnell"]
            current_model_idx = 0
            self.append_log(f"硅基流动自动模式（🔄 免费模型轮换）：目标 {target_total} 张，当前随机关键词 {total_keywords} 条")
        else:
            self.append_log(f"硅基流动自动模式：目标 {target_total} 张，当前随机关键词 {total_keywords} 条")

        while self.is_generating and generated < target_total:
            if idx >= len(prompts):
                # 用完一轮关键词后，从头再循环使用，不再重新生成新关键词
                idx = 0

            prompt = prompts[idx]
            idx += 1

            for _ in range(images_per_prompt):
                if not self.is_generating or generated >= target_total:
                    break

                current_seed = random.randint(1, 999999)
                
                # 如果开启了自动轮换，使用当前轮换的模型
                if auto_rotate:
                    current_model = free_models[current_model_idx]
                    model_name = "Kolors" if "Kolors" in current_model else "FLUX.1-schnell"
                    # 轮换到下一个模型
                    current_model_idx = (current_model_idx + 1) % len(free_models)
                else:
                    current_model = model
                    model_name = "Kolors" if "Kolors" in model else "FLUX"

                def _progress_cb(msg, _self=self):
                    nonlocal generated
                    _self.append_log(msg)
                    if msg.startswith("✓ 图片已保存:"):
                        generated += 1
                        if auto_rotate:
                            _self.append_log(f"✓ [{generated}/{target_total}] 已保存（模型：{model_name}）")
                        else:
                            _self.append_log(f"✓ [{generated}/{target_total}] 已保存")

                # 同样，这里不再依赖返回值来判断是否成功
                self.generator.generate_image(
                    prompt=prompt,
                    width=width,
                    height=height,
                    model=current_model,
                    num_inference_steps=num_inference_steps,
                    guidance_scale=guidance_scale,
                    progress_callback=_progress_cb,
                )

        self.append_log(f"任务结束：累计生成 {generated}/{target_total} 张图片")
    
    def start_generation(self):
        if self.is_generating:
            return

        # 一进入就标记为“正在生成”，并更新按钮状态，防止重复点击触发多个线程
        self.is_generating = True
        self.generate_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        self.stop_btn.setStyleSheet(self.stop_btn_enabled_style)

        def reset_generating_state():
            """在启动失败或提前返回时，恢复按钮和状态。"""
            self.is_generating = False
            self.generate_btn.setEnabled(True)
            self.stop_btn.setEnabled(False)
            self.stop_btn.setStyleSheet(self.stop_btn_disabled_style)
        
        # 验证保存文件夹
        if not self.save_folder:
            QMessageBox.warning(self, "警告", "请选择图片保存文件夹")
            reset_generating_state()
            return
        
        # 获取选中的服务
        is_siliconflow = self.siliconflow_radio.isChecked()
        
        # 如果是 SiliconFlow，检查 API Keys
        if is_siliconflow:
            if not self.saved_api_keys:
                QMessageBox.warning(self, "警告", "请先配置 SiliconFlow API Key")
                reset_generating_state()
                return
        
        # 获取参数
        prompts = []
        
        if self.auto_gen_checkbox.isChecked():
            # 自动生成模式：每次点击“开始生成”时都刷新一批全新的随机关键词
            try:
                self.append_log("🔄 开始生成前，自动刷新一批随机关键词...")
                self.generate_and_display_random_prompts()
            except Exception as e:
                self.append_log(f"生成随机关键词失败: {e}")
                reset_generating_state()
                return
            # 刷新后再从输入框读取本次要用的关键词
            prompt_text = self.prompt_edit.toPlainText().strip()
            if not prompt_text:
                QMessageBox.warning(self, "警告", "自动生成随机关键词失败，请稍后重试")
                reset_generating_state()
                return
            prompts = [p.strip() for p in prompt_text.split("\n") if p.strip()]
            self.append_log(f"自动生成模式：当前 {len(prompts)} 个随机关键词（本次已刷新）")
        else:
            # 手动输入模式
            prompt_text = self.prompt_edit.toPlainText().strip()
            if not prompt_text:
                QMessageBox.warning(self, "警告", "请输入图片关键词")
                reset_generating_state()
                return
            prompts = [p.strip() for p in prompt_text.split("\n") if p.strip()]
        
        if not prompts:
            QMessageBox.warning(self, "警告", "没有有效的关键词")
            reset_generating_state()
            return
        
        # 获取参数
        width = self.width_spin.value()
        height = self.height_spin.value()
        seed = self.seed_spin.value()
        model = self.model_combo.currentText()
        images_per_prompt = self.count_spin.value()
        
        # 检查是否开启自动轮换免费模型
        auto_rotate = is_siliconflow and self.auto_rotate_models_checkbox.isChecked()
        
        # 根据配置决定是否走本地代理（ShadowsocksX，SOCKS5）
        proxy_dict = None
        try:
            from config import config as app_config
            proxy_enabled = app_config.get("network.proxy_enabled", False)
            if proxy_enabled:
                host = app_config.get("network.socks5_host", "127.0.0.1")
                port = int(app_config.get("network.socks5_port", 1086))
                proxy_url = f"socks5h://{host}:{port}"
                try:
                    import socks  # type: ignore  # noqa: F401
                    proxy_dict = {"http": proxy_url, "https": proxy_url}
                    self.append_log(f"🌐 已启用代理下载图片：{proxy_url}")
                except Exception:
                    self.append_log("⚠ 勾选了代理，但未安装 PySocks（pip install 'requests[socks]'），将改为直连下载")
        except Exception:
            proxy_dict = None
        
        # 根据服务创建生成器
        if is_siliconflow:
            # 使用保存的 API Keys
            if len(self.saved_api_keys) > 1:
                self.generator = SiliconFlowGenerator(self.saved_api_keys, self.save_folder, proxies=proxy_dict)
                self.append_log(f"使用服务：硅基流动（{len(self.saved_api_keys)} 个 API Key 轮换）")
            else:
                self.generator = SiliconFlowGenerator(self.saved_api_keys[0], self.save_folder, proxies=proxy_dict)
                self.append_log("使用服务：硅基流动（SiliconFlow）")
            # 显示使用的模型
            if auto_rotate:
                self.append_log("使用模型：🔄 自动轮换（Kolors ⇄ FLUX.1-schnell）")
            else:
                model_name = "Kolors" if "Kolors" in model else "FLUX"
                self.append_log(f"使用模型：{model_name} ({model})")
        else:
            self.generator = PollinationsGenerator(self.save_folder, proxies=proxy_dict)
            self.append_log("使用服务：Pollinations.AI")
            self.append_log(f"使用模型/风格：{model}")

        # 目标总张数（在 Pollinations 自动模式下使用；其它模式可忽略）
        try:
            target_total = max(1, int(self.total_images_spin.value()))
        except Exception:
            target_total = 100000
        # 保存到配置，方便下次打开沿用
        try:
            from config import config as app_config
            app_config.set("pollinations.target_total", target_total)
        except Exception:
            pass
        
        if is_siliconflow:
            if self.auto_gen_checkbox.isChecked():
                # 硅基流动自动模式：按目标总数控制
                if auto_rotate:
                    self.append_log(f"开始生成任务：硅基流动自动模式（🔄 免费模型轮换），当前 {len(prompts)} 个随机关键词，目标 {target_total} 张图片")
                else:
                    self.append_log(f"开始生成任务：硅基流动自动模式，当前 {len(prompts)} 个随机关键词，目标 {target_total} 张图片")
            else:
                # 普通批量模式：总数由关键词数量和每个关键词生成张数共同决定
                self.append_log(f"开始生成任务：{len(prompts)} 个关键词，每个 {images_per_prompt} 张，共 {len(prompts) * images_per_prompt} 张图片")
        else:
            if self.auto_gen_checkbox.isChecked():
                # Pollinations 自动随机模式：按目标总数控制
                self.append_log(f"开始生成任务：Pollinations 自动模式，当前 {len(prompts)} 个随机关键词，目标 {target_total} 张图片")
            else:
                # Pollinations 普通批量模式：按关键词数 * 每个关键词生成张数
                self.append_log(f"开始生成任务：{len(prompts)} 个关键词，每个 {images_per_prompt} 张，共 {len(prompts) * images_per_prompt} 张图片")
        
        self.append_log(f"参数：分辨率 {width}x{height}")
        
        # 在后台线程中生成
        import threading
        def generate_thread():
            try:
                if is_siliconflow:
                    if self.auto_gen_checkbox.isChecked():
                        # 硅基流动自动随机关键词模式：持续生成直到达到目标总数
                        self._run_siliconflow_auto_generation(
                            prompts=prompts,
                            width=width,
                            height=height,
                            model=model,
                            images_per_prompt=images_per_prompt,
                            target_total=target_total,
                            num_inference_steps=20,
                            guidance_scale=7.5,
                            auto_rotate=auto_rotate,
                        )
                    else:
                        # SiliconFlow 普通批量模式（手动输入关键词）
                        self.generator.generate_batch(
                            prompts=prompts,
                            width=width,
                            height=height,
                            model=model,
                            images_per_prompt=images_per_prompt,
                            num_inference_steps=20,
                            guidance_scale=7.5,
                            # 通过信号到主线程追加日志
                            progress_callback=self.append_log,
                            should_stop=lambda: not self.is_generating  # 停止检查
                        )
                else:
                    if self.auto_gen_checkbox.isChecked():
                        # Pollinations 自动随机关键词模式：持续生成直到达到目标总数
                        self._run_pollinations_auto_generation(
                            prompts=prompts,
                            width=width,
                            height=height,
                            seed=seed,
                            model=model,
                            images_per_prompt=images_per_prompt,
                            target_total=target_total,
                        )
                    else:
                        # Pollinations 普通批量模式（手动输入关键词）
                        self.generator.generate_batch(
                            prompts=prompts,
                            width=width,
                            height=height,
                            seed=seed,
                            model=model,
                            images_per_prompt=images_per_prompt,
                            auto_seed=True,
                            # 通过信号到主线程追加日志
                            progress_callback=self.append_log,
                            should_stop=lambda: not self.is_generating  # 停止检查
                        )
            except Exception as e:
                self.append_log(f"生成过程出错: {str(e)}")
            finally:
                self.is_generating = False
                self.generate_btn.setEnabled(True)
                self.stop_btn.setEnabled(False)
                # 恢复停止按钮样式
                self.stop_btn.setStyleSheet(self.stop_btn_disabled_style)
        
        thread = threading.Thread(target=generate_thread, daemon=True)
        thread.start()
    
    def stop_generation(self):
        self.is_generating = False
        self.append_log("⛔ 用户已停止生成任务")
        self.generate_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        # 恢复停止按钮样式
        self.stop_btn.setStyleSheet(self.stop_btn_disabled_style)


# 延时配置对话框
# 主窗口
class RankingWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.api = KuaishouAPI()
        self.api.data_received.connect(self.on_data_received)
        self.api.error_occurred.connect(self.on_error)
        # 将商品列表请求日志输出到首页日志控制台
        try:
            if hasattr(self.api, 'progress_log'):
                self.api.progress_log.connect(self.on_api_progress_log)
        except Exception:
            pass
        
        # 任务停止标记（用于抑制停止后的完成弹窗）
        self._crawl_stopped_by_user = False
        self._download_stopped_by_user = False
        
        # 从配置文件加载已有的Cookie设置
        saved_cookies = config.get("cookies.list", [])
        if saved_cookies:
            self.api.update_cookies(saved_cookies)
            print(f"从配置文件加载了 {len(saved_cookies)} 个Cookie")
        if saved_cookies and isinstance(saved_cookies, list):
            self.api.update_cookies(saved_cookies)
        
        # 数据库配置 - 从配置文件读取
        self.db_config = DatabaseConfig()
        
        # 当前筛选条件
        self.current_filters = {
            "layoutType": "4",
            "configurationId": "2",
            "domainType": "total",  # 全部/直播榜/短视频榜/店铺榜
            "channelId": 99,        # 总榜/食品饮料/家居百货等
            "rankType": "1h",       # 小时榜/日榜/周榜/月榜
            "pcursor": "0",
            "curRankNum": 0
        }
        
        # 佣金筛选区间（未设置则不过滤）
        self.commission_min = None
        self.commission_max = None
        
        # 商品数据
        self.goods_list = []
        self.has_more = True
        
        # 当前显示的详情窗口（如果有）
        self.detail_window = None
        
        # Cookie配置对话框引用
        self.cookie_dialog = None
        
        # 数据抓取和下载相关对象
        self.crawler = None
        self.downloader = None
        self.crawl_progress_dialog = None
        self.download_progress_dialog = None
        
        # 风控重试任务管理器 - 使用配置文件中的数据库配置
        retry_db_config = DatabaseConfig()
        self.retry_manager = RiskControlRetryManager(retry_db_config, self.api)
        self.retry_control_dialog = None
        self.downloader = None
        self.crawl_progress_dialog = None
        self.download_progress_dialog = None
        
        self.setup_ui()
        self.setup_filters()
        self.load_initial_data()
        
        # 在UI设置完成后启动API服务器
        self.start_api_server()
        
    def start_api_server(self):
        """启动FastAPI服务器用于与浏览器插件通信"""
        try:
            from api_server import start_api_server, set_gui_window_instance
            # 设置GUI窗口实例，用于日志重定向
            set_gui_window_instance(self)
            # 在后台线程中启动API服务器
            api_thread = threading.Thread(target=start_api_server, args=("127.0.0.1", 8767), daemon=True)
            api_thread.start()
            self.add_log_message("✅ FastAPI服务器已在后台启动: http://127.0.0.1:8767", "#4ade80")
        except Exception as e:
            error_msg = f"⚠️ FastAPI服务器启动失败: {e}"
            print(error_msg)
            self.add_log_message(error_msg, "#f87171")
            print("将使用备用方案（直接打开浏览器）")
            self.add_log_message("将使用备用方案（直接打开浏览器）", "#fbbf24")
        
    def setup_ui(self):
        self.setWindowTitle("快手商品排行榜")
        # 设置应用图标（窗口/任务栏）
        try:
            icon_path = resource_path("app.ico")
            if os.path.exists(icon_path):
                self.setWindowIcon(QIcon(icon_path))
        except Exception:
            pass
        # 固定尺寸按 DPI 缩放
        sw, sh = scaled_size(1200, 800)
        self.setFixedSize(sw, sh)
        # 保险：通过 WinAPI 强制设置任务栏/标题栏图标
        try:
            import ctypes
            hwnd = int(self.winId())
            icon_path = resource_path("app.ico")
            if os.path.exists(icon_path):
                LR_LOADFROMFILE = 0x00000010
                LR_DEFAULTSIZE = 0x00000040
                IMAGE_ICON = 1
                hicon = ctypes.windll.user32.LoadImageW(None, icon_path, IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE)
                WM_SETICON = 0x0080
                ICON_SMALL = 0
                ICON_BIG = 1
                ctypes.windll.user32.SendMessageW(hwnd, WM_SETICON, ICON_SMALL, hicon)
                ctypes.windll.user32.SendMessageW(hwnd, WM_SETICON, ICON_BIG, hicon)
        except Exception:
            pass
        # 设置窗口居中显示
        from PyQt5.QtWidgets import QDesktopWidget
        screen = QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width() - size.width()) // 2, (screen.height() - size.height()) // 2)
        
        # 创建菜单栏
        menubar = self.menuBar()
        if menubar is not None:
            # 创建主分类菜单
            self.category_menu = menubar.addMenu('主分类')
            
            # 创建榜单类型菜单
            self.domain_menu = menubar.addMenu('榜单类型')
            
            # 创建时间范围菜单
            self.time_menu = menubar.addMenu('时间范围')
            
            # 创建配置菜单
            self.config_menu = menubar.addMenu('配置')
            
            # 添加Cookie配置选项
            self.cookie_action = QAction('配置Cookie', self)
            self.cookie_action.triggered.connect(self.show_cookie_config)
            if self.config_menu is not None:
                self.config_menu.addAction(self.cookie_action)
                
            # 添加数据库配置选项
            self.db_config_action = QAction('数据库配置', self)
            self.db_config_action.triggered.connect(self.show_db_config)
            if self.config_menu is not None:
                self.config_menu.addAction(self.db_config_action)
            
            # 创建定时任务菜单
            self.task_menu = menubar.addMenu('定时任务')
            
            # 添加数据抓取选项
            self.crawl_action = QAction('启动数据抓取任务', self)
            self.crawl_action.triggered.connect(self.start_crawl_task)
            if self.task_menu is not None:
                self.task_menu.addAction(self.crawl_action)
                
            # 添加视频下载选项
            self.download_action = QAction('启动视频下载任务', self)
            self.download_action.triggered.connect(self.start_download_task)
            if self.task_menu is not None:
                self.task_menu.addAction(self.download_action)
                
            # 添加分隔线
            if self.task_menu is not None:
                self.task_menu.addSeparator()
            
            # 添加风控重试任务控制选项
            self.retry_control_action = QAction('风控重试任务控制', self)
            self.retry_control_action.triggered.connect(self.open_retry_control)
            if self.task_menu is not None:
                self.task_menu.addAction(self.retry_control_action)
            
            # 添加调试日志开关选项
            self.debug_log_action = QAction('启用详细日志', self)
            self.debug_log_action.setCheckable(True)
            self.debug_log_action.setChecked(False)  # 默认关闭
            self.debug_log_action.triggered.connect(self.toggle_debug_logging)
            if self.task_menu is not None:
                self.task_menu.addAction(self.debug_log_action)
            
            # 添加榜单类型选项
            self.domain_actions = []
            domain_types = [
                ("全部", "total"),
                ("直播榜", "live"),
                ("短视频榜", "video"),
                ("店铺榜", "wind")
            ]
            for text, value in domain_types:
                action = QAction(text, self)
                action.setCheckable(True)
                action.triggered.connect(lambda checked, v=value: self.on_domain_type_changed(v))
                if self.domain_menu is not None:
                    self.domain_menu.addAction(action)
                self.domain_actions.append((action, value))
            
            # 设置默认选中全部
            if self.domain_actions:
                self.domain_actions[0][0].setChecked(True)
            
            # 添加时间范围选项
            self.time_actions = []
            time_ranges = [
                ("小时榜", "1h"),
                ("日榜", "1d"),
                ("周榜", "7d"),
                ("月榜", "30d")
            ]
            for text, value in time_ranges:
                action = QAction(text, self)
                action.setCheckable(True)
                action.triggered.connect(lambda checked, v=value: self.on_time_range_changed(v))
                if self.time_menu is not None:
                    self.time_menu.addAction(action)
                self.time_actions.append((action, value))
            
            # 设置默认选中小时榜
            if self.time_actions:
                self.time_actions[0][0].setChecked(True)
        
        # 创建中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        root_layout = QVBoxLayout(central_widget)
        root_layout.setSpacing(10)
        root_layout.setContentsMargins(10, 10, 10, 10)

        # 创建Tab
        from PyQt5.QtWidgets import QTabWidget
        self.main_tabs = QTabWidget()
        
        # 美化 Tab 样式
        self.main_tabs.setStyleSheet("""
            QTabWidget::pane {
                border: 1px solid #e5e7eb;
                border-radius: 8px;
                background-color: #ffffff;
                top: -1px;
            }
            
            QTabBar::tab {
                background-color: #f3f4f6;
                color: #6b7280;
                border: 1px solid #e5e7eb;
                border-bottom: none;
                border-top-left-radius: 8px;
                border-top-right-radius: 8px;
                padding: 10px 20px;
                margin-right: 4px;
                font-size: 14px;
                font-weight: 500;
                min-width: 100px;
            }
            
            QTabBar::tab:selected {
                background-color: #ffffff;
                color: #2563eb;
                border-bottom: 2px solid #2563eb;
                font-weight: 600;
            }
            
            QTabBar::tab:hover:!selected {
                background-color: #e5e7eb;
                color: #374151;
            }
            
            QTabBar::tab:first {
                margin-left: 0;
            }
        """)
        
        root_layout.addWidget(self.main_tabs)

        # 首页Tab
        homepage = QWidget()
        home_layout = QVBoxLayout(homepage)
        home_layout.setSpacing(10)
        home_layout.setContentsMargins(0, 0, 0, 0)

        # 佣金筛选 + 加载更多 控制条
        controls_row = QHBoxLayout()
        controls_row.setSpacing(6)  # 减小水平间距
        controls_row.setContentsMargins(0, 5, 0, 5)  # 设置较小的上下边距
        
        # 佣金区间标签
        label_commission = QLabel("佣金区间")
        label_commission.setStyleSheet("""
            QLabel {
                font-size: 14px;
                color: #333;
                font-weight: 500;
                padding: 2px 4px;  /* 减小内边距 */
            }
        """)
        
        # 最小佣金输入框
        self.commission_min_spin = QLineEdit()
        self.commission_min_spin.setText("0")
        self.commission_min_spin.setFixedSize(70, 28)
        self.commission_min_spin.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.commission_min_spin.setStyleSheet("""
            QLineEdit {
                border: 1px solid #e1e5e9;
                border-radius: 6px;
                padding: 3px 6px;
                font-size: 13px;
                background-color: white;
            }
            QLineEdit:focus {
                border: 1px solid #4a90e2;
                outline: none;
            }
        """)
        
        # 第一个百分号
        percent_label1 = QLabel("%")
        percent_label1.setStyleSheet("""
            QLabel {
                font-size: 14px;
                color: #666;
                font-weight: 500;
                padding: 2px 4px;  /* 减小内边距 */
            }
        """)
        
        # 分隔符
        sep_label = QLabel("-")
        sep_label.setStyleSheet("""
            QLabel {
                font-size: 16px;
                color: #666;
                font-weight: bold;
                padding: 2px 6px;  /* 减小内边距 */
            }
        """)
        
        # 最大佣金输入框
        self.commission_max_spin = QLineEdit()
        self.commission_max_spin.setText("100")
        self.commission_max_spin.setFixedSize(70, 28)
        self.commission_max_spin.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.commission_max_spin.setStyleSheet("""
            QLineEdit {
                border: 1px solid #e1e5e9;
                border-radius: 6px;
                padding: 3px 6px;
                font-size: 13px;
                background-color: white;
            }
            QLineEdit:focus {
                border: 1px solid #4a90e2;
                outline: none;
            }
        """)
        
        # 第二个百分号
        percent_label2 = QLabel("%")
        percent_label2.setStyleSheet("""
            QLabel {
                font-size: 14px;
                color: #666;
                font-weight: 500;
                padding: 2px 4px;  /* 减小内边距 */
            }
        """)
        
        # 连接文本变化信号到实时筛选
        self.commission_min_spin.textChanged.connect(self.on_commission_filter_changed)
        self.commission_max_spin.textChanged.connect(self.on_commission_filter_changed)
        
        # 加载更多按钮
        self.load_more_btn = QPushButton("加载更多")
        self.load_more_btn.clicked.connect(self.load_more_data)
        self.load_more_btn.setStyleSheet("""
            QPushButton {
                background-color: #4a90e2;
                color: white;
                border: none;
                border-radius: 6px;
                padding: 6px 12px;  /* 减小内边距 */
                font-size: 13px;
                font-weight: 500;
            }
            QPushButton:hover {
                background-color: #357abd;
            }
            QPushButton:pressed {
                background-color: #2968a3;
            }
        """)
        
        # 添加控件到布局（靠右显示）
        controls_row.addStretch()  # 先添加弹性空间，把其他控件推到右侧
        controls_row.addWidget(label_commission)
        controls_row.addSpacing(6)  # 减小间距
        controls_row.addWidget(self.commission_min_spin)
        controls_row.addWidget(percent_label1)
        controls_row.addWidget(sep_label)
        controls_row.addWidget(self.commission_max_spin)
        controls_row.addWidget(percent_label2)
        controls_row.addSpacing(12)  # 减小间距
        controls_row.addWidget(self.load_more_btn)
        
        home_layout.addLayout(controls_row)

        # 商品列表
        self.goods_list_widget = QListWidget()
        self.goods_list_widget.setFrameStyle(QFrame.NoFrame)
        self.goods_list_widget.setVerticalScrollMode(QListWidget.ScrollPerPixel)
        scrollbar = self.goods_list_widget.verticalScrollBar()
        if scrollbar is not None:
            scrollbar.valueChanged.connect(self.on_scroll)
        home_layout.addWidget(self.goods_list_widget)

        # 日志控制台
        self.log_console = QTextEdit()
        self.log_console.setMaximumHeight(180)
        self.log_console.setReadOnly(True)
        # 使用与风控重试任务一致的暗色日志样式
        self.log_console.setStyleSheet("""
            QTextEdit {
                background-color: #2b2b2b;
                color: #ffffff;
                font-family: 'Consolas', 'Monaco', monospace;
                font-size: 12px;
                border: 1px solid #555;
                border-radius: 6px;
                padding: 8px;
            }
        """)
        self.log_console.hide()
        home_layout.addWidget(self.log_console)

        # 加载提示
        self.loading_label = QLabel("加载中...")
        self.loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.loading_label.hide()
        home_layout.addWidget(self.loading_label)

        self.main_tabs.addTab(homepage, "爆款排行榜")

        # 爆款数据抓取Tab
        self.crawl_panel = CrawlConfigPanel(self)
        self.main_tabs.addTab(self.crawl_panel, "爆款数据抓取")
        
        # 爆款视频下载Tab
        self.download_panel = DownloadConfigPanel(self)
        self.main_tabs.addTab(self.download_panel, "爆款视频下载")

        # 爆款商品列表Tab
        self.goods_list_panel = GoodsListPanel(self)
        self.main_tabs.addTab(self.goods_list_panel, "爆款商品")

        # 风控重试Tab
        self.retry_control_dialog = RetryTaskControlDialog(self.retry_manager, self)
        self.main_tabs.addTab(self.retry_control_dialog, "风控重试")

        # 素材榜数据抓取Tab
        self.material_crawl_panel = MaterialCrawlPanel(self)
        self.main_tabs.addTab(self.material_crawl_panel, "素材榜数据抓取")

        # 素材榜视频下载Tab
        self.material_download_panel = MaterialDownloadPanel(self)
        self.main_tabs.addTab(self.material_download_panel, "素材榜视频下载")
        
        # AI 图片下载Tab
        self.pollinations_panel = PollinationsPanel(self)
        self.main_tabs.addTab(self.pollinations_panel, "图片下载")
        
        # 连接商品列表请求日志到首页日志控制台
        try:
            if hasattr(self.api, 'progress_log'):
                self.api.progress_log.connect(self.on_api_progress_log)
        except Exception:
            pass
        
    def on_api_progress_log(self, message: str):
        # 首页日志控制台显示API请求日志
        try:
            self.add_log_message(message, "#9CA3AF")
        except Exception:
            pass

    def add_log_message(self, message, color="#ffffff"):
        """添加日志消息到控制台"""
        # 添加时间戳
        from datetime import datetime
        timestamp = datetime.now().strftime("%H:%M:%S")
        
        # 添加带颜色的日志信息
        log_entry = f'<span style="color: {color};">[{timestamp}] {message}</span>'
        self.log_console.append(log_entry)
        
        # 滚动到最新日志
        self.log_console.moveCursor(QTextCursor.End)
        
    def show_toast(self, message: str, duration_ms: int = 2000):
        """显示自动消失的消息提示（非弹窗）"""
        try:
            if not hasattr(self, "_toast_label") or self._toast_label is None:
                self._toast_label = QLabel(self)
                self._toast_label.setStyleSheet(
                    """
                    QLabel {
                        background-color: rgba(0, 0, 0, 180);
                        color: #ffffff;
                        padding: 8px 14px;
                        border-radius: 14px;
                        font-weight: 600;
                    }
                    """
                )
                self._toast_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
                self._toast_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
            self._toast_label.setText(message)
            self._toast_label.adjustSize()
            # 放在窗口顶部中间
            x = (self.width() - self._toast_label.width()) // 2
            y = 60
            self._toast_label.move(x, y)
            self._toast_label.show()
            self._toast_label.raise_()
            # 定时隐藏
            QTimer.singleShot(duration_ms, lambda: self._toast_label.hide())
        except Exception:
            # 兜底到日志
            self.add_log_message(message, "#fbbf24")

    def get_display_goods(self):
        """返回应用佣金筛选后的商品列表"""
        if self.commission_min is None or self.commission_max is None:
            return list(self.goods_list)
        min_v = float(self.commission_min)
        max_v = float(self.commission_max)
        result = []
        for g in self.goods_list:
            v = g.get("commissionRate")
            try:
                v = float(v)
            except Exception:
                continue
            if min_v <= v <= max_v:
                result.append(g)
        return result

    def on_commission_filter_changed(self):
        """佣金筛选数值变化时的实时筛选"""
        try:
            min_text = self.commission_min_spin.text().strip()
            max_text = self.commission_max_spin.text().strip()
            
            # 如果为空或非数字，使用默认值
            try:
                v1 = int(min_text) if min_text else 0
            except ValueError:
                v1 = 0
                
            try:
                v2 = int(max_text) if max_text else 100
            except ValueError:
                v2 = 100
                
            # 确保数值在合理范围内
            v1 = max(0, min(100, v1))
            v2 = max(0, min(100, v2))
            
            if v1 > v2:
                v1, v2 = v2, v1
                
            self.commission_min, self.commission_max = v1, v2
            # 仅刷新视图（不重新请求）
            self.refresh_goods_view()
        except Exception as e:
            self.add_log_message(f"筛选失败: {e}", "#f87171")

    def apply_commission_filter(self):
        """保留方法兼容性，用于其他代码调用"""
        self.on_commission_filter_changed()
        # 触发一次加载更多
        self.load_more_data()

    def refresh_goods_view(self):
        """根据当前筛选条件刷新商品列表视图"""
        try:
            scrollbar = self.goods_list_widget.verticalScrollBar()
            if scrollbar is not None:
                try:
                    scrollbar.valueChanged.disconnect(self.on_scroll)
                except Exception:
                    pass
            # 不清空数据，直接重用并更新
            current_count = self.goods_list_widget.count()
            display_list = self.get_display_goods()
            for i, goods_data in enumerate(display_list):
                if i < current_count:
                    item = self.goods_list_widget.item(i)
                    existing_card = self.goods_list_widget.itemWidget(item)
                    if not existing_card or existing_card.goods_data.get('relItemId') != goods_data.get('relItemId'):
                        new_card = GoodsCard(goods_data)
                        if item is not None:
                            item.setSizeHint(new_card.sizeHint())
                        self.goods_list_widget.setItemWidget(item, new_card)
                else:
                    item = QListWidgetItem()
                    card = GoodsCard(goods_data)
                    item.setSizeHint(card.sizeHint())
                    self.goods_list_widget.addItem(item)
                    self.goods_list_widget.setItemWidget(item, card)
            while self.goods_list_widget.count() > len(display_list):
                self.goods_list_widget.takeItem(self.goods_list_widget.count() - 1)
            if scrollbar is not None:
                scrollbar.valueChanged.connect(self.on_scroll)
        except Exception as e:
            self.add_log_message(f"刷新列表失败: {e}", "#f87171")

    def on_domain_type_changed(self, domain_type):
        """当榜单类型菜单选项改变时调用"""
        # 确保只有一个榜单类型被选中
        for action, value in self.domain_actions:
            action.setChecked(value == domain_type)
            
        self.current_filters["domainType"] = domain_type
        self.load_initial_data()
        
    def on_main_category_changed(self, index):
        """当主分类下拉框的选项改变时调用"""
        channel_id = self.main_category_combo.itemData(index)
        self.current_filters["channelId"] = channel_id
        self.load_initial_data()
        
    def on_time_range_changed(self, rank_type):
        """当时间范围菜单选项改变时调用"""
        # 确保只有一个时间范围被选中
        for action, value in self.time_actions:
            action.setChecked(value == rank_type)
            
        self.current_filters["rankType"] = rank_type
        self.load_initial_data()
        
    def configure_cookie(self):
        """配置Cookie"""
        # 导入对话框相关模块
        from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QPushButton, QHBoxLayout
        
        # 创建Cookie配置对话框
        if self.cookie_dialog is None:
            self.cookie_dialog = QDialog(self)
            self.cookie_dialog.setWindowTitle("配置Cookie")
            self.cookie_dialog.resize(600, 400)
            
            layout = QVBoxLayout()
            
            # 添加说明标签
            label = QLabel("请在此处粘贴您的Cookie信息：")
            layout.addWidget(label)
            
            # 添加文本编辑框
            self.cookie_text_edit = QTextEdit()
            self.cookie_text_edit.setPlainText(self.api.cookie)
            layout.addWidget(self.cookie_text_edit)
            
            # 添加按钮布局
            button_layout = QHBoxLayout()
            
            # 确定按钮
            ok_button = QPushButton("确定")
            ok_button.clicked.connect(self.save_cookie)
            button_layout.addWidget(ok_button)
            
            # 取消按钮
            cancel_button = QPushButton("取消")
            def close_dialog():
                if self.cookie_dialog is not None:
                    self.cookie_dialog.close()
            cancel_button.clicked.connect(close_dialog)
            button_layout.addWidget(cancel_button)
            
            layout.addLayout(button_layout)
            self.cookie_dialog.setLayout(layout)
        
        self.cookie_dialog.show()
        self.cookie_dialog.raise_()
        self.cookie_dialog.activateWindow()
        
    def save_cookie(self):
        """保存Cookie配置"""
        new_cookie = self.cookie_text_edit.toPlainText()
        self.api.update_cookie(new_cookie)
        
        # 同时更新商品详情API的Cookie
        try:
            # 如果有已经创建的详情窗口，也需要更新它们的API
            if hasattr(self, 'detail_window') and self.detail_window is not None:
                self.detail_window.api.update_cookie(new_cookie)
        except:
            pass
        
        # 关闭对话框
        if self.cookie_dialog is not None:
            self.cookie_dialog.close()
        
        # 重新加载数据
        self.load_initial_data()
        
    def show_cookie_config(self):
        """显示Cookie配置对话框"""
        dialog = CookieConfigDialog(self.api, self)
        if dialog.exec_() == QDialog.Accepted:
            if dialog.save_cookies():
                QMessageBox.information(self, '配置成功', 'Cookie配置已保存')
            else:
                QMessageBox.warning(self, '配置失败', 'Cookie配置保存失败')
    
    def show_db_config(self):
        """显示数据库配置对话框"""
        dialog = DatabaseConfigDialog(self.db_config, self)
        if dialog.exec_() == QDialog.Accepted:
            self.db_config = dialog.get_config()
            QMessageBox.information(self, '配置成功', '数据库配置已保存')
    
    def configure_database(self):
        """配置数据库 (保留旧方法兼容性)"""
        self.show_db_config()
    
    def start_crawl_task(self):
        """切换到数据抓取标签页"""
        if hasattr(self, 'main_tabs'):
            # 切到“数据抓取”标签
            for i in range(self.main_tabs.count()):
                if self.main_tabs.tabText(i) == "数据抓取":
                    self.main_tabs.setCurrentIndex(i)
                    break
    
    def start_crawl_from_panel(self, config):
        """从Tab面板启动数据抓取"""
        # 记录最近配置用于自动调度
        self.last_crawl_config = config
        # 重置停止标记
        self._crawl_stopped_by_user = False
        # 清除数据或直接抓取
        cleanup_option = config.get("cleanup_option", "不清除数据")
        if cleanup_option != "不清除数据":
            self.start_data_cleanup(cleanup_option, config)
        else:
            self.start_data_crawler(config)
        
    def start_data_cleanup(self, cleanup_option, crawl_config):
        """启动数据清除任务"""
        # 使用最新的数据库配置
        current_db_config = DatabaseConfig()
        
        # 创建数据清除任务
        self.cleanup_task = DataCleanupTask(current_db_config, cleanup_option)
        # 连接进度到面板
        if hasattr(self, 'crawl_panel') and self.crawl_panel is not None:
            self.cleanup_task.progress_updated.connect(self.crawl_panel.on_progress)
        self.cleanup_task.cleanup_finished.connect(lambda: self.on_cleanup_finished(crawl_config))
        self.cleanup_task.error_occurred.connect(self.on_cleanup_error)
        
        # 启动清除任务
        self.cleanup_task.start()
        
    def on_cleanup_finished(self, crawl_config):
        """数据清除完成后启动数据抓取"""
        if self.crawl_progress_dialog is not None:
            self.crawl_progress_dialog.update_progress("数据清除完成，开始数据抓取...")
        # 启动数据抓取
        self.start_data_crawler(crawl_config)
    
    def on_cleanup_error(self, error_msg):
        """数据清除出错处理"""
        if self.crawl_progress_dialog:
            self.crawl_progress_dialog.close()
        QMessageBox.critical(self, "错误", f"数据清除出错: {error_msg}")
        
    def start_data_crawler(self, config):
        """启动数据抓取任务"""
        # 记录最近配置，用于自动调度
        self.last_crawl_config = config
        # 创建数据抓取器 - 使用最新的数据库配置
        # 确保使用配置文件中的最新数据库配置
        current_db_config = DatabaseConfig()
        self.crawler = DataCrawlerAPI(current_db_config, self.api)
        # 设置调试状态
        self.crawler.set_debug_enabled(DEBUG_LOGGING_ENABLED)
        # 将进度输出到数据抓取面板
        if hasattr(self, 'crawl_panel') and self.crawl_panel is not None:
            self.crawl_panel.set_task_object(self.crawler)
            self.crawl_panel.task_started()
            self.crawler.progress_updated.connect(self.crawl_panel.on_progress)
            # 实时统计信号，避免解析日志延迟
            try:
                self.crawler.counts_updated.connect(self.crawl_panel.on_counts_updated)
            except Exception:
                pass
        self.crawler.crawl_finished.connect(self.on_crawl_finished)
        self.crawler.error_occurred.connect(self.on_crawl_error)
        
        # 启动抓取任务
        # 如果没有选中任何类目，则category_names为None，DataCrawlerAPI会使用默认的12大类目
        selected_categories = config.get("selected_categories")
        self.crawler.start_crawl(
            category_names=selected_categories,  # 传递选中的类目名称列表
            date_range=config["date_range"],
            rank_type=config["rank_type"]
        )
            
    def on_crawl_finished(self):
        """数据抓取完成"""
        # 面板完成提示
        if hasattr(self, 'crawl_panel') and self.crawl_panel is not None:
            self.crawl_panel.task_finished()
        # 更新当日累计统计（无论是否人为停止都累计）
        try:
            from datetime import datetime
            today = datetime.now().strftime('%Y%m%d')
            stats_date = config.get("stats.crawl.date", today)
            goods_sum = config.get("stats.crawl.goods", 0)
            videos_sum = config.get("stats.crawl.videos", 0)
            if stats_date != today:
                goods_sum, videos_sum = 0, 0
                config.set("stats.crawl.date", today)
            goods_sum += getattr(self.crawler, 'total_saved_goods', 0)
            videos_sum += getattr(self.crawler, 'total_saved_videos', 0)
            config.set("stats.crawl.goods", goods_sum)
            config.set("stats.crawl.videos", videos_sum)
            self.add_log_message(f"今日累计抓取：{goods_sum} 个商品，{videos_sum} 个视频URL", "#60a5fa")
            if hasattr(self, 'crawl_panel') and self.crawl_panel is not None:
                self.crawl_panel.append_log(f"今日累计抓取：{goods_sum} 个商品，{videos_sum} 个视频URL")
        except Exception:
            pass
        # 如果是用户点击停止导致结束，则仅写“停止”日志且不弹提示/不自动调度
        if getattr(self, '_crawl_stopped_by_user', False):
            self._crawl_stopped_by_user = False
            try:
                self.crawl_panel.append_log("任务已停止")
            except Exception:
                pass
            return
        # 自动抓取逻辑
        auto_enabled = config.get("crawl.auto.enabled", False)
        try:
            if isinstance(auto_enabled, dict):
                auto_enabled = auto_enabled.get("value", False)
        except Exception:
            pass
        interval_minutes = config.get("crawl.auto.interval_minutes", 0)
        try:
            interval_minutes = int(interval_minutes if not isinstance(interval_minutes, dict) else interval_minutes.get("value", 0))
        except Exception:
            interval_minutes = 0
        if auto_enabled and interval_minutes > 0 and hasattr(self, 'last_crawl_config') and self.last_crawl_config:
            # 日志提示（首页日志控制台）
            self.add_log_message(f"本次抓取完毕，已启用{interval_minutes}分钟后再次抓取")
            # 同步输出到数据抓取面板日志
            try:
                if hasattr(self, 'crawl_panel') and self.crawl_panel is not None:
                    self.crawl_panel.append_log(f"将于 {interval_minutes} 分钟后自动启动下一次抓取任务")
            except Exception:
                pass
            # 定时再次启动
            QTimer.singleShot(interval_minutes * 60 * 1000, lambda: self.start_data_crawler(self.last_crawl_config))
        else:
            # 不弹窗，只在日志中输出
            self.add_log_message("✨ 数据抓取任务已完成", "#10b981")
            if hasattr(self, 'crawl_panel') and self.crawl_panel is not None:
                self.crawl_panel.append_log("✨ 数据抓取任务已完成")
        
    def on_crawl_error(self, error_msg):
        """数据抓取出错"""
        if self.crawl_progress_dialog:
            self.crawl_progress_dialog.close()
        # 不弹窗，只在日志中输出
        self.add_log_message(f"❌ 数据抓取出错: {error_msg}", "#ef4444")
        if hasattr(self, 'crawl_panel') and self.crawl_panel is not None:
            self.crawl_panel.append_log(f"❌ 数据抓取出错: {error_msg}")
        
    def start_download_task(self):
        """切换到视频下载标签页"""
        if hasattr(self, 'main_tabs'):
            for i in range(self.main_tabs.count()):
                if self.main_tabs.tabText(i) == "视频下载":
                    self.main_tabs.setCurrentIndex(i)
                    break
        
    def start_material_crawl_from_panel(self, cfg):
        """从Tab面板启动素材榜数据抓取"""
        # 记录最近配置
        self.last_material_crawl_config = cfg
        # 创建数据库配置
        current_db_config = DatabaseConfig()
        # 使用素材爬虫
        self.material_crawler = MaterialCrawler(current_db_config)
        # 从配置中加载 Cookies
        cookies_list = config.get('material_cookies.list', [])
        if cookies_list:
            self.material_crawler.set_cookies(cookies_list)
        # 将进度输出到素材榜抓取面板
        if hasattr(self, 'material_crawl_panel') and self.material_crawl_panel is not None:
            self.material_crawl_panel.set_task_object(self.material_crawler)
            self.material_crawler.progress_updated.connect(self.material_crawl_panel.on_progress)
        self.material_crawler.crawl_finished.connect(self.on_material_crawl_finished)
        self.material_crawler.error_occurred.connect(self.on_material_crawl_error)
        # 设置过滤条件
        filters = None
        try:
            if isinstance(cfg, dict):
                filters = cfg.get("filters")
        except Exception:
            filters = None
        self.material_crawler.start_crawl(filters)
        if hasattr(self, 'material_crawl_panel') and self.material_crawl_panel is not None:
            self.material_crawl_panel.task_started()

    def on_material_crawl_finished(self):
        """素材榜数据抓取完成"""
        if hasattr(self, 'material_crawl_panel') and self.material_crawl_panel is not None:
            self.material_crawl_panel.task_finished()
            # 不弹窗，只在日志中输出
            self.material_crawl_panel.append_log("✨ 素材榜数据抓取任务已完成")

    def on_material_crawl_error(self, error_msg):
        """素材榜数据抓取出错"""
        # 不弹窗，只在日志中输出
        if hasattr(self, 'material_crawl_panel') and self.material_crawl_panel is not None:
            self.material_crawl_panel.append_log(f"❌ 素材榜数据抓取出错: {error_msg}")
        
    def start_download_from_panel(self, config):
        """从Tab面板启动视频下载（商品榜）"""
        self.last_download_config = config
        # 重置停止标记
        self._download_stopped_by_user = False
        if not config.get("download_path"):
            QMessageBox.warning(self, "警告", "请选择下载路径")
            return
        # 创建视频下载器 - 使用最新的数据库配置
        current_db_config = DatabaseConfig()
        self.downloader = VideoDownloader(
            current_db_config,
            config["download_path"],
            config["thread_count"],
            ignore_commission_filter=config.get("no_commission_filter", False)
        )
        # 设置调试状态
        self.downloader.set_debug_enabled(DEBUG_LOGGING_ENABLED)
        # 输出到下载面板
        if hasattr(self, 'download_panel') and self.download_panel is not None:
            self.download_panel.set_task_object(self.downloader)
            self.download_panel.task_started()
            self.downloader.progress_updated.connect(self.download_panel.on_progress)
        self.downloader.download_finished.connect(self.on_download_finished)
        self.downloader.error_occurred.connect(self.on_download_error)
        
        # 启动下载任务
        self.downloader.start()

    def start_material_download_from_panel(self, config):
        """从 Tab面板启动素材榜视频下载"""
        self.last_material_download_config = config
        if not config.get("download_path"):
            QMessageBox.warning(self, "警告", "请选择下载路径")
            return
        current_db_config = DatabaseConfig()
        # 从配置中提取筛选条件
        filters = config.get("filters") or {}
        self.material_downloader = MaterialVideoDownloader(
            current_db_config,
            config["download_path"],
            config["thread_count"],
            filters=filters,
        )
        if hasattr(self, 'material_download_panel') and self.material_download_panel is not None:
            self.material_download_panel.set_task_object(self.material_downloader)
            self.material_download_panel.task_started()
            self.material_downloader.progress_updated.connect(self.material_download_panel.on_progress)
        self.material_downloader.download_finished.connect(self.on_material_download_finished)
        self.material_downloader.error_occurred.connect(self.on_material_download_error)
        self.material_downloader.start()
        
    def on_download_finished(self):
        """视频下载完成"""
        # 面板完成提示
        if hasattr(self, 'download_panel') and self.download_panel is not None:
            self.download_panel.task_finished()
        # 更新当日累计下载统计（无论是否人为停止都累计）
        try:
            from datetime import datetime
            today = datetime.now().strftime('%Y%m%d')
            d_date = config.get("stats.download.date", today)
            d_videos = config.get("stats.download.videos", 0)
            if d_date != today:
                d_videos = 0
                config.set("stats.download.date", today)
            d_videos += getattr(self.downloader, 'total_downloaded_videos', 0)
            config.set("stats.download.videos", d_videos)
            self.add_log_message(f"今日累计下载：{d_videos} 个视频", "#34d399")
            if hasattr(self, 'download_panel') and self.download_panel is not None:
                self.download_panel.append_log(f"今日累计下载：{d_videos} 个视频")
        except Exception:
            pass
        # 如果是用户点击停止导致结束，则仅写“停止”日志且不弹提示/不自动调度
        if getattr(self, '_download_stopped_by_user', False):
            self._download_stopped_by_user = False
            try:
                self.download_panel.append_log("任务已停止")
            except Exception:
                pass
            return
        # 自动下载逻辑
        auto_enabled = config.get("download.auto.enabled", False)
        try:
            if isinstance(auto_enabled, dict):
                auto_enabled = auto_enabled.get("value", False)
        except Exception:
            pass
        interval_minutes = config.get("download.auto.interval_minutes", 0)
        try:
            interval_minutes = int(interval_minutes if not isinstance(interval_minutes, dict) else interval_minutes.get("value", 0))
        except Exception:
            interval_minutes = 0
        if auto_enabled and interval_minutes > 0 and hasattr(self, 'last_download_config') and self.last_download_config:
            # 日志提示（首页日志控制台）
            self.add_log_message(f"本次下载完毕，已启用{interval_minutes}分钟后再次下载")
            # 同步输出到“视频下载”面板日志
            try:
                if hasattr(self, 'download_panel') and self.download_panel is not None:
                    self.download_panel.append_log(f"将于 {interval_minutes} 分钟后自动启动下一次下载任务")
            except Exception:
                pass
            QTimer.singleShot(interval_minutes * 60 * 1000, lambda: self.start_download_from_panel(self.last_download_config))
        else:
            # 不弹窗，只在日志中输出
            self.add_log_message("✨ 视频下载任务已完成", "#10b981")
            if hasattr(self, 'download_panel') and self.download_panel is not None:
                self.download_panel.append_log("✨ 视频下载任务已完成")
        
    def on_material_download_finished(self):
        """素材榜视频下载完成"""
        if hasattr(self, 'material_download_panel') and self.material_download_panel is not None:
            self.material_download_panel.task_finished()
            # 不弹窗，只在日志中输出
            self.material_download_panel.append_log("✨ 素材榜视频下载任务已完成")

    def on_material_download_error(self, error_msg):
        """素材榜视频下载出错"""
        # 不弹窗，只在日志中输出
        if hasattr(self, 'material_download_panel') and self.material_download_panel is not None:
            self.material_download_panel.append_log(f"❌ 素材榜视频下载出错: {error_msg}")
        
    def on_download_error(self, error_msg):
        """视频下载出错"""
        if self.download_progress_dialog:
            self.download_progress_dialog.close()
        # 不弹窗，只在日志中输出
        self.add_log_message(f"❌ 视频下载出错: {error_msg}", "#ef4444")
        if hasattr(self, 'download_panel') and self.download_panel is not None:
            self.download_panel.append_log(f"❌ 视频下载出错: {error_msg}")
        
    def toggle_debug_logging(self, checked):
        """切换调试日志显示"""
        global DEBUG_LOGGING_ENABLED
        DEBUG_LOGGING_ENABLED = checked
        status = "启用" if checked else "禁用"
        print(f"调试日志已{status}")
        
        # 控制日志控制台的显示/隐藏
        if checked:
            self.log_console.show()
        else:
            self.log_console.hide()
        
        # 同时更新正在运行的任务的调试状态
        if hasattr(self, 'crawler') and self.crawler is not None:
            self.crawler.set_debug_enabled(checked)
        if hasattr(self, 'downloader') and self.downloader is not None:
            self.downloader.set_debug_enabled(checked)
        if hasattr(self, 'retry_manager') and self.retry_manager is not None:
            self.retry_manager.set_debug_enabled(checked)
            
    def open_retry_control(self):
        """切换到风控重试标签页"""
        if hasattr(self, 'main_tabs'):
            for i in range(self.main_tabs.count()):
                if self.main_tabs.tabText(i) == "风控重试":
                    self.main_tabs.setCurrentIndex(i)
                    break
        
    def on_data_received(self, data):
        # 重置加载标志
        self.is_loading = False
        self.loading_label.hide()
        
        # 检查API响应是否成功
        if data.get("result") != 1:
            self.on_error("数据获取失败: " + data.get("error_msg", "未知错误"))
            return
            
        # 加载分类数据
        self.load_categories_from_data(data)
            
        # 解析商品数据
        new_goods_cards = []
        data_section = data.get("data", {})
        data_items = data_section.get("data", {})
        
        # 查找所有商品卡片数据
        for key, value in data_items.items():
            if key.startswith("CpsGoodsCard_") and "goodsData" in value.get("fields", {}):
                goods_data = value["fields"]["goodsData"]
                # 添加当前时间范围到商品数据中
                goods_data['rankType'] = self.current_filters.get("rankType", "1h")
                new_goods_cards.append(goods_data)
                
        # 更新是否有更多数据
        global_data = data_section.get("global", {}).get("commonData", {})
        pcursor = global_data.get("pcursor", "0")
        self.has_more = pcursor != "0" and len(new_goods_cards) > 0
        
        # 判断是否是初始化加载
        is_initial_load = self.current_filters.get("pcursor", "0") == "0"

        # 如果是加载更多但没有任何新数据，提示没有更多
        if not is_initial_load and len(new_goods_cards) == 0:
            try:
                self.show_toast("没有更多数据了")
            except Exception:
                pass
        
        # 如果是初始化加载，清空商品列表
        if is_initial_load:
            self.goods_list = []
        
        # 将新商品添加到列表中
        for goods_data in new_goods_cards:
            self.goods_list.append(goods_data)
        
        # 对整个商品列表按销量排序（从高到低）
        def get_sales_value(goods_data):
            sales_info = goods_data.get('salesVolumeDesc', {})
            sales_value = sales_info.get('value', '0')
            try:
                # 确保返回数值用于排序，支持整数和浮点数
                if '.' in sales_value:
                    numeric_value = float(sales_value)
                else:
                    numeric_value = int(sales_value)
                return numeric_value
            except (ValueError, TypeError):
                # 如果转换失败，返回0
                return 0
        
        # 确保所有榜单类型都按销量排序
        self.goods_list.sort(key=get_sales_value, reverse=True)
        
        # 重新渲染整个列表 - 优化卡片复用机制避免重复加载图片
        # 先断开信号连接，避免在更新过程中触发事件
        scrollbar = self.goods_list_widget.verticalScrollBar()
        if scrollbar is not None:
            try:
                scrollbar.valueChanged.disconnect(self.on_scroll)
            except:
                pass  # 如果没有连接，忽略错误
        
        # 优化更新策略：只在初始加载时清空列表
        if is_initial_load:
            self.goods_list_widget.clear()
            
        # 获取当前列表中的项目数量
        current_count = self.goods_list_widget.count()

        # 计算需要展示的列表（应用佣金筛选）
        display_list = self.get_display_goods()
        
        # 添加新项目或更新现有项目
        for i, goods_data in enumerate(display_list):
            if i < current_count:
                # 更新现有项目
                item = self.goods_list_widget.item(i)
                existing_card = self.goods_list_widget.itemWidget(item)
                
                # 检查数据是否相同，如果不同则更新
                if not existing_card or existing_card.goods_data.get('relItemId') != goods_data.get('relItemId'):
                    new_card = GoodsCard(goods_data)
                    if item is not None:
                        item.setSizeHint(new_card.sizeHint())
                    self.goods_list_widget.setItemWidget(item, new_card)
            else:
                # 添加新项目
                item = QListWidgetItem()
                card = GoodsCard(goods_data)
                item.setSizeHint(card.sizeHint())
                self.goods_list_widget.addItem(item)
                self.goods_list_widget.setItemWidget(item, card)

        # 如果新数据比现有项目少，删除多余的项目
        while self.goods_list_widget.count() > len(display_list):
            self.goods_list_widget.takeItem(self.goods_list_widget.count() - 1)
        
        # 重新连接信号
        if scrollbar is not None:
            scrollbar.valueChanged.connect(self.on_scroll)
        
    def on_error(self, error_msg):
        self.loading_label.hide()
        print(f"错误: {error_msg}")
        # 这里可以添加错误提示对话框
        
    def on_scroll(self, value):
        # 实现滚动加载更多，但增加防抖机制
        if not self.has_more:
            return
            
        # 防抖处理，避免频繁触发
        if hasattr(self, '_scroll_timer'):
            self._scroll_timer.stop()
        else:
            from PyQt5.QtCore import QTimer
            self._scroll_timer = QTimer()
            self._scroll_timer.setSingleShot(True)
            self._scroll_timer.timeout.connect(self._check_scroll_position)
        
        self._current_scroll_value = value
        self._scroll_timer.start(300)  # 300ms防抖延迟
            
    def _check_scroll_position(self):
        """检查滚动位置并决定是否加载更多"""
        if not hasattr(self, '_current_scroll_value'):
            return
            
        scrollbar = self.goods_list_widget.verticalScrollBar()
        if scrollbar and self._current_scroll_value >= scrollbar.maximum() - 50:  # 接近底部时加载更多
            self.load_more_data()
            
    def setup_filters(self):
        # 初始化分类数据（从API返回的数据中加载）
        self.categories = []
        
    def load_categories_from_data(self, data):
        """从API返回的数据中提取分类信息"""
        try:
            # 从API响应中提取分类数据
            data_section = data.get("data", {})
            data_items = data_section.get("data", {})
            
            # 查找category_filter数据
            category_filter = None
            for key, value in data_items.items():
                if key == "category_filter" and "fields" in value:
                    category_filter = value["fields"]
                    break
                    
            if category_filter and "filterList" in category_filter:
                # 只有当分类数据发生变化时才更新
                new_categories = category_filter["filterList"]
                if hasattr(self, 'categories') and self.categories != new_categories:
                    self.categories = new_categories
                    print(f"成功加载 {len(self.categories)} 个主分类:")
                    for category in self.categories:
                        sub_count = len(category.get('subList', []))
                        print(f"  - {category['name']} (ID: {category['id']}) - {sub_count} 个子分类")
                    
                    # 更新菜单显示
                    self.update_category_menu()
                elif not hasattr(self, 'categories'):
                    self.categories = new_categories
                    print(f"成功加载 {len(self.categories)} 个主分类:")
                    for category in self.categories:
                        sub_count = len(category.get('subList', []))
                        print(f"  - {category['name']} (ID: {category['id']}) - {sub_count} 个子分类")
                    
                    # 更新菜单显示
                    self.update_category_menu()
                
        except Exception as e:
            print(f"解析分类数据时出错: {e}")
            
    def update_category_menu(self):
        """根据API返回的分类数据更新菜单"""
        # 清空现有的主分类菜单项
        if hasattr(self, 'category_menu') and self.category_menu is not None:
            self.category_menu.clear()
        
        # 添加分类选项
        self.category_actions = []
        if hasattr(self, 'categories') and self.categories:
            # 先创建所有菜单项但不添加到菜单中
            actions_map = {}
            submenus_map = {}
            
            for category in self.categories:
                action = QAction(category['name'], self)
                action.setCheckable(True)
                channel_id = category['id']
                # 为有子分类的菜单项添加悬停事件
                if category.get('subList', []):
                    # 创建子菜单
                    submenu = QMenu()
                    sub_actions = []
                    for sub_category in category['subList']:
                        sub_action = QAction(sub_category['name'], self)
                        sub_action.setCheckable(True)
                        sub_channel_id = sub_category['id']
                        # 存储子分类ID以便后续使用
                        sub_action.setData(sub_channel_id)
                        sub_action.triggered.connect(
                            lambda checked, cid=channel_id, scid=sub_channel_id, name=sub_category['name']: 
                                self.on_sub_category_changed(cid, scid, name)
                        )
                        sub_actions.append((sub_action, sub_channel_id))
                        submenu.addAction(sub_action)
                    submenus_map[channel_id] = (submenu, sub_actions)
                    action.setMenu(submenu)
                
                # 存储主分类ID
                action.setData(channel_id)
                action.triggered.connect(lambda checked, cid=channel_id: self.on_category_changed(cid))
                actions_map[channel_id] = action
                
            # 按原始顺序添加菜单项
            for category in self.categories:
                channel_id = category['id']
                action = actions_map.get(channel_id)
                if action and self.category_menu is not None:
                    self.category_menu.addAction(action)
                self.category_actions.append((action, channel_id))
                
                # 设置默认选中总榜
                if channel_id == 99 and action:
                    action.setChecked(True)
        else:
            # 如果没有分类数据，添加默认选项
            action = QAction("总榜", self)
            action.setCheckable(True)
            action.setChecked(True)
            action.setData(99)
            action.triggered.connect(lambda checked: self.on_category_changed(99))
            if self.category_menu is not None:
                self.category_menu.addAction(action)
            self.category_actions = [(action, 99)]
            
        # 更新主分类菜单的选中状态
        current_channel_id = self.current_filters.get("channelId", 99)
        if hasattr(self, 'category_actions'):
            for action, value in self.category_actions:
                if action:
                    action.setChecked(value == current_channel_id)
                    
        # 同时更新子菜单的选中状态
        current_sub_channel_id = self.current_filters.get("subChannelId")
        if current_sub_channel_id and hasattr(self, 'category_menu') and self.category_menu is not None:
            for action in self.category_menu.actions():
                submenu = action.menu()
                if submenu:  # 如果有子菜单
                    for sub_action in submenu.actions():
                        # 设置子菜单项的选中状态
                        if hasattr(sub_action, 'data'):
                            sub_action.setChecked(sub_action.data() == current_sub_channel_id)
                    
    def on_category_changed(self, channel_id):
        """当主分类菜单选项改变时调用"""
        # 确保只有一个分类被选中
        for action, value in self.category_actions:
            action.setChecked(value == channel_id)
            
        self.current_filters["channelId"] = channel_id
        # 清除子分类ID
        if "subChannelId" in self.current_filters:
            del self.current_filters["subChannelId"]
        self.load_initial_data()
        
        # 更新子菜单的选中状态（取消所有子菜单的选中状态）
        if hasattr(self, 'category_menu') and self.category_menu is not None:
            for action in self.category_menu.actions():
                submenu = action.menu()
                if submenu:  # 如果有子菜单
                    for sub_action in submenu.actions():
                        sub_action.setChecked(False)
                        
    def on_sub_category_changed(self, channel_id, sub_channel_id, sub_name):
        """当子分类菜单选项改变时调用"""
        # 设置主分类和子分类
        self.current_filters["channelId"] = channel_id
        self.current_filters["subChannelId"] = sub_channel_id
        self.current_filters["categoryName"] = sub_name
        
        # 更新主分类菜单的选中状态
        current_channel_id = self.current_filters.get("channelId", 99)
        if hasattr(self, 'category_actions'):
            for action, value in self.category_actions:
                if action:
                    action.setChecked(value == current_channel_id)
                    
        # 同时更新子菜单的选中状态
        current_sub_channel_id = self.current_filters.get("subChannelId")
        if current_sub_channel_id and hasattr(self, 'category_menu') and self.category_menu is not None:
            for action in self.category_menu.actions():
                submenu = action.menu()
                if submenu:  # 如果有子菜单
                    for sub_action in submenu.actions():
                        # 设置子菜单项的选中状态
                        if hasattr(sub_action, 'data'):
                            sub_action.setChecked(sub_action.data() == current_sub_channel_id)
        
        self.load_initial_data()
        
    def show_goods_detail(self, rel_item_id):
        """显示商品详情页面"""
        try:
            # 导入商品详情页面模块
            from final_product_detail import ProductDetailWindow
            
            # 如果已经有详情窗口，先关闭它
            if self.detail_window:
                self.detail_window.close()
                self.detail_window = None
            
            # 创建商品详情窗口
            self.detail_window = ProductDetailWindow(rel_item_id)
            
            # 隐藏商品列表，显示详情页面和返回按钮
            self.stacked_widget.hide()
            self.back_button.show()
            
            # 显示详情窗口
            self.detail_window.show()
            
        except Exception as e:
            print(f"显示商品详情页面失败: {e}")
            
    def show_goods_list(self):
        """返回商品列表页面"""
        # 隐藏详情窗口
        if self.detail_window:
            self.detail_window.close()
            self.detail_window = None
            
        # 显示商品列表，隐藏返回按钮
        self.stacked_widget.show()
        self.back_button.hide()
        
    def load_initial_data(self):
        self.goods_list_widget.clear()
        self.goods_list = []
        self.current_filters["pcursor"] = "0"
        self.current_filters["curRankNum"] = 0
        self.request_data()
        
    def request_data(self):
        self.loading_label.show()
        self.api.get_rank_data(self.current_filters)
        
    # 停止标记接口（供面板调用）
    def mark_crawl_stopped(self):
        self._crawl_stopped_by_user = True
    
    def mark_download_stopped(self):
        self._download_stopped_by_user = True
        
    def load_more_data(self):
        # 手动加载更多
        if not self.has_more:
            try:
                self.show_toast("没有更多数据了")
            except Exception:
                pass
            return
        # 安全保护：超过80后不再请求（默认最多100条）
        try:
            cur = int(self.current_filters.get("curRankNum", 0))
        except Exception:
            cur = 0
        if cur >= 80:
            self.has_more = False
            self.loading_label.hide()
            try:
                self.show_toast("没有更多数据了")
            except Exception:
                pass
            return
        self.loading_label.show()
        self.current_filters["pcursor"] = str(int(self.current_filters["pcursor"]) + 20)
        self.current_filters["curRankNum"] = int(self.current_filters["curRankNum"]) + 20
        self.api.load_more(self.current_filters)

def main():
    # 高分屏适配
    try:
        from PyQt5.QtCore import QCoreApplication
        QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
        QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
        # macOS specific: Prevent crash with modal dialogs
        if sys.platform == 'darwin':
            QCoreApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
    except Exception:
        pass
    # Windows 任务栏图标与分组（使用可执行名作 AppUserModelID）
    try:
        import ctypes
        exe_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
        app_id = f"{exe_name}.app"
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
    except Exception:
        pass
    app = QApplication(sys.argv)
    try:
        app_icon_path = resource_path("app.ico")
        if os.path.exists(app_icon_path):
            app.setWindowIcon(QIcon(app_icon_path))
    except Exception:
        pass
    window = RankingWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()